• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / brighterscript / 28445188414

30 Jun 2026 12:43PM UTC coverage: 86.869%. First build
28445188414

Pull #1739

github

web-flow
Merge a6070b5dc into 0d91ee47f
Pull Request #1739: Merge master into v1

16105 of 19568 branches covered (82.3%)

Branch coverage included in aggregate %.

139 of 183 new or added lines in 13 files covered. (75.96%)

16853 of 18372 relevant lines covered (91.73%)

27723.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.81
/src/bscPlugin/validation/BrsFileValidator.ts
1
import { isAliasStatement, isBlock, isBody, isCallExpression, isClassStatement, isConditionalCompileConstStatement, isConditionalCompileErrorStatement, isConditionalCompileStatement, isConstStatement, isDottedGetExpression, isDottedSetStatement, isEnumStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isIfStatement, isImportStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceStatement, isInvalidType, isLibraryStatement, isLiteralExpression, isMethodStatement, isNamespaceStatement, isTypecastExpression, isTypecastStatement, isTypedFunctionTypeExpression, isTypeStatement, isUnaryExpression, isVariableExpression, isVoidType, isWhileStatement } from '../../astUtils/reflection';
1✔
2
import { createVisitor, WalkMode } from '../../astUtils/visitors';
1✔
3
import { DiagnosticMessages } from '../../DiagnosticMessages';
1✔
4
import type { BrsFile } from '../../files/BrsFile';
5
import type { ExtraSymbolData, ValidateFileEvent } from '../../interfaces';
6
import { TokenKind, UnreferencableBuiltins } from '../../lexer/TokenKind';
1✔
7
import type { AstNode, Expression, Statement } from '../../parser/AstNode';
8
import { CallExpression, type FunctionExpression, type LiteralExpression } from '../../parser/Expression';
1✔
9
import { ParseMode } from '../../parser/Parser';
1✔
10
import type { ContinueStatement, EnumMemberStatement, EnumStatement, ForEachStatement, ForStatement, ImportStatement, LibraryStatement, Body, WhileStatement, TypecastStatement, Block, AliasStatement, IfStatement, ConditionalCompileStatement } from '../../parser/Statement';
11
import { SymbolTypeFlag } from '../../SymbolTypeFlag';
1✔
12
import { AssociativeArrayType } from '../../types/AssociativeArrayType';
1✔
13
import { DynamicType } from '../../types/DynamicType';
1✔
14
import util from '../../util';
1✔
15
import type { Range } from 'vscode-languageserver';
16
import type { Token } from '../../lexer/Token';
17
import type { BrightScriptDoc } from '../../parser/BrightScriptDocParser';
18
import brsDocParser from '../../parser/BrightScriptDocParser';
1✔
19
import { TypeStatementType } from '../../types/TypeStatementType';
1✔
20
import * as semver from 'semver';
1✔
21
import { OPTIONAL_CHAINING_MIN_FIRMWARE_VERSION } from '../../RokuConstants';
1✔
22
import type { AvailabilityAxis } from '../../DiagnosticMessages';
23
import { globalCallableMap } from '../../globalCallables';
1✔
24

25
export class BrsFileValidator {
1✔
26
    constructor(
27
        public event: ValidateFileEvent<BrsFile>
2,479✔
28
    ) {
29
    }
30

31

32
    public process() {
33
        const unlinkGlobalSymbolTable = this.event.file.parser.symbolTable.pushParentProvider(() => this.event.program.globalScope.symbolTable);
10,827✔
34

35
        util.validateTooDeepFile(this.event.file);
2,479✔
36

37
        // Invalidate cache on this file
38
        // It could have potentially changed before this from plugins, after this, it will not change
39
        // eslint-disable-next-line @typescript-eslint/dot-notation
40
        this.event.file['_cachedLookups'].invalidate();
2,479✔
41

42
        // make a copy of the bsConsts, because they might be added to
43
        const bsConstsBackup = new Map<string, boolean>(this.event.file.ast.getBsConsts());
2,479✔
44

45
        this.walk();
2,479✔
46
        this.flagTopLevelStatements();
2,479✔
47
        //only validate the file if it was actually parsed (skip files containing typedefs)
48
        if (!this.event.file.hasTypedef) {
2,479✔
49
            this.validateTopOfFileStatements();
2,478✔
50
            this.validateTypecastStatements();
2,478✔
51
        }
52

53
        this.event.file.ast.bsConsts = bsConstsBackup;
2,479✔
54
        unlinkGlobalSymbolTable();
2,479✔
55
    }
56

57
    /**
58
     * Walk the full AST
59
     */
60
    private walk() {
61
        const isBrighterscript = this.event.file.parser.options.mode === ParseMode.BrighterScript;
2,479✔
62

63
        const visitor = createVisitor({
2,479✔
64
            MethodStatement: (node) => {
65
                //add the `super` symbol to class methods
66
                if (isClassStatement(node.parent) && node.parent.hasParentClass()) {
260✔
67
                    const data: ExtraSymbolData = {};
75✔
68
                    const parentClassType = node.parent.parentClassName.getType({ flags: SymbolTypeFlag.typetime, data: data });
75✔
69
                    node.func.body.getSymbolTable().addSymbol('super', { ...data, isInstance: true }, parentClassType, SymbolTypeFlag.runtime);
75✔
70
                }
71
            },
72
            CallfuncExpression: (node) => {
73
                if (node.args.length > 5) {
70✔
74
                    this.event.program.diagnostics.register({
1✔
75
                        ...DiagnosticMessages.callfuncHasToManyArgs(node.args.length),
76
                        location: node.tokens.methodName.location
77
                    });
78
                }
79
            },
80
            DottedGetExpression: (node) => {
81
                if (node.tokens.dot?.kind === TokenKind.QuestionDot) {
1,922!
82
                    this.validateMinFirmwareVersionForOptionalChaining(node.tokens.dot?.location?.range);
23!
83
                }
84
            },
85
            IndexedGetExpression: (node) => {
86
                if (node.tokens.questionDot || node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare) {
88!
87
                    const range = node.tokens.questionDot?.location?.range ?? node.tokens.openingSquare?.location?.range;
9!
88
                    this.validateMinFirmwareVersionForOptionalChaining(range);
9✔
89
                }
90
            },
91
            CallExpression: (node) => {
92
                if (node.tokens.openingParen?.kind === TokenKind.QuestionLeftParen) {
1,257✔
93
                    this.validateMinFirmwareVersionForOptionalChaining(node.tokens.openingParen?.location?.range);
5!
94
                }
95
                this.validateGlobalCallableAvailability(node);
1,257✔
96
            },
97
            EnumStatement: (node) => {
98
                this.validateDeclarationLocations(node, 'enum', () => util.createBoundingRange(node.tokens.enum, node.tokens.name));
168✔
99

100
                this.validateEnumDeclaration(node);
168✔
101

102
                if (!node.tokens.name) {
168!
103
                    return;
×
104
                }
105
                //register this enum declaration
106
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime });
168✔
107
                // eslint-disable-next-line no-bitwise
108
                node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, { definingNode: node }, nodeType, SymbolTypeFlag.typetime | SymbolTypeFlag.runtime);
168!
109
            },
110
            ClassStatement: (node) => {
111
                if (!node?.tokens?.name) {
458!
112
                    return;
1✔
113
                }
114
                this.validateDeclarationLocations(node, 'class', () => util.createBoundingRange(node.tokens.class, node.tokens.name));
457✔
115

116
                //register this class
117
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime });
457✔
118
                node.getSymbolTable().addSymbol('m', { definingNode: node, isInstance: true }, nodeType, SymbolTypeFlag.runtime);
457✔
119
                // eslint-disable-next-line no-bitwise
120
                node.parent.getSymbolTable()?.addSymbol(node.tokens.name?.text, { definingNode: node }, nodeType, SymbolTypeFlag.typetime | SymbolTypeFlag.runtime);
457!
121

122
                if (node.findAncestor(isNamespaceStatement)) {
457✔
123
                    //add the transpiled name for namespaced constructors to the root symbol table
124
                    const transpiledClassConstructor = node.getName(ParseMode.BrightScript);
138✔
125

126
                    this.event.file.parser.ast.symbolTable.addSymbol(
138✔
127
                        transpiledClassConstructor,
128
                        { definingNode: node },
129
                        node.getConstructorType(),
130
                        // eslint-disable-next-line no-bitwise
131
                        SymbolTypeFlag.runtime | SymbolTypeFlag.postTranspile
132
                    );
133
                }
134
            },
135
            AssignmentStatement: (node) => {
136
                if (!node?.tokens?.name) {
980!
137
                    return;
×
138
                }
139
                if (isForStatement(node.parent) && node.parent.counterDeclaration === node) {
980✔
140
                    // for loop variable variable is added to the block symbol table elsewhere
141
                    return;
27✔
142
                }
143
                const data: ExtraSymbolData = {};
953✔
144
                //register this variable
145
                let nodeType = node.getType({ flags: SymbolTypeFlag.runtime, data: data });
953✔
146
                if (isInvalidType(nodeType) || isVoidType(nodeType)) {
953✔
147
                    nodeType = DynamicType.instance;
11✔
148
                }
149
                node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true, isFromDocComment: data.isFromDocComment, isFromCallFunc: data.isFromCallFunc }, nodeType, SymbolTypeFlag.runtime);
953!
150
            },
151
            DottedSetStatement: (node) => {
152
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
108✔
153
            },
154
            IndexedSetStatement: (node) => {
155
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
19✔
156
            },
157
            ForEachStatement: (node) => {
158
                //registering the for loop variable happens in the visitor for Block, since the loop variable is scoped to the loop body
159
            },
160
            NamespaceStatement: (node) => {
161
                if (!node?.nameExpression) {
664!
162
                    return;
×
163
                }
164
                this.validateDeclarationLocations(node, 'namespace', () => util.createBoundingRange(node.tokens.namespace, node.nameExpression));
664✔
165
                //Namespace Types are added at the Scope level - This is handled when the SymbolTables get linked
166
            },
167
            FunctionStatement: (node) => {
168
                this.validateDeclarationLocations(node, 'function', () => util.createBoundingRange(node.func.tokens.functionType, node.tokens.name));
2,455✔
169
                const funcType = node.getType({ flags: SymbolTypeFlag.typetime });
2,455✔
170

171
                if (node.tokens.name?.text) {
2,455!
172
                    node.parent.getSymbolTable().addSymbol(
2,455✔
173
                        node.tokens.name.text,
174
                        { definingNode: node },
175
                        funcType,
176
                        SymbolTypeFlag.runtime
177
                    );
178
                }
179

180
                const namespace = node.findAncestor(isNamespaceStatement);
2,455✔
181
                //this function is declared inside a namespace
182
                if (namespace) {
2,455✔
183
                    namespace.getSymbolTable().addSymbol(
427✔
184
                        node.tokens.name?.text,
1,281!
185
                        { definingNode: node },
186
                        funcType,
187
                        SymbolTypeFlag.runtime
188
                    );
189
                    if (!node.tokens?.name) {
427!
190
                        return;
×
191
                    }
192
                    //add the transpiled name for namespaced functions to the root symbol table
193
                    const transpiledNamespaceFunctionName = node.getName(ParseMode.BrightScript);
427✔
194

195
                    this.event.file.parser.ast.symbolTable.addSymbol(
427✔
196
                        transpiledNamespaceFunctionName,
197
                        { definingNode: node },
198
                        funcType,
199
                        // eslint-disable-next-line no-bitwise
200
                        SymbolTypeFlag.runtime | SymbolTypeFlag.postTranspile
201
                    );
202
                }
203
            },
204
            FunctionExpression: (node) => {
205
                const funcSymbolTable = node.getSymbolTable();
2,769✔
206
                const isInlineFunc = !(isFunctionStatement(node.parent) || isMethodStatement(node.parent));
2,769✔
207
                if (isInlineFunc) {
2,769✔
208
                    // symbol table should not include any symbols from parent func
209
                    funcSymbolTable.pushParentProvider(() => node.findAncestor<Body>(isBody).getSymbolTable());
220✔
210
                }
211
                if (!funcSymbolTable?.hasSymbol('m', SymbolTypeFlag.runtime) || isInlineFunc) {
2,769!
212
                    if (!isTypecastStatement(node.body?.statements?.[0])) {
54!
213
                        // if this is an inline function, or if the function body does not start with a typecast statement, add the `m` symbol to the function scope. If the function body starts with a typecast statement, the `m` symbol will be added in the visitor for TypecastStatement, and it will be typed as the type from the typecast statement
214
                        funcSymbolTable?.addSymbol('m', { isInstance: true }, new AssociativeArrayType(), SymbolTypeFlag.runtime);
53!
215
                    }
216
                }
217
                this.validateFunctionParameterCount(node);
2,769✔
218
            },
219
            FunctionParameterExpression: (node) => {
220
                if (isTypedFunctionTypeExpression(node.parent)) {
1,402✔
221
                    return;
35✔
222
                }
223
                const paramName = node.tokens?.name?.text;
1,367!
224
                if (!paramName) {
1,367!
225
                    return;
×
226
                }
227
                const data: ExtraSymbolData = {};
1,367✔
228
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime, data: data });
1,367✔
229
                // add param symbol at expression level, so it can be used as default value in other params
230
                const funcExpr = node.findAncestor<FunctionExpression>(isFunctionExpression);
1,367✔
231
                const funcSymbolTable = funcExpr?.getSymbolTable();
1,367!
232
                const extraSymbolData: ExtraSymbolData = {
1,367✔
233
                    definingNode: node,
234
                    isInstance: true,
235
                    isFromDocComment: data.isFromDocComment,
236
                    description: data.description
237
                };
238
                funcSymbolTable?.addSymbol(paramName, extraSymbolData, nodeType, SymbolTypeFlag.runtime);
1,367!
239

240
                //also add param symbol at block level, as it may be redefined, and if so, should show a union
241
                funcExpr.body.getSymbolTable()?.addSymbol(paramName, extraSymbolData, nodeType, SymbolTypeFlag.runtime);
1,367!
242
            },
243
            InterfaceStatement: (node) => {
244
                if (!node.tokens.name) {
209!
245
                    return;
×
246
                }
247
                this.validateDeclarationLocations(node, 'interface', () => util.createBoundingRange(node.tokens.interface, node.tokens.name));
209✔
248

249
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime });
209✔
250
                // eslint-disable-next-line no-bitwise
251
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node }, nodeType, SymbolTypeFlag.typetime);
209✔
252
            },
253
            ConstStatement: (node) => {
254
                if (!node.tokens.name) {
207!
255
                    return;
×
256
                }
257
                this.validateDeclarationLocations(node, 'const', () => util.createBoundingRange(node.tokens.const, node.tokens.name));
207✔
258
                const nodeType = node.getType({ flags: SymbolTypeFlag.runtime });
207✔
259
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true }, nodeType, SymbolTypeFlag.runtime);
207✔
260
            },
261
            CatchStatement: (node) => {
262
                //brs and bs both support variableExpression for the exception variable
263
                if (isVariableExpression(node.exceptionVariableExpression)) {
15✔
264
                    node.parent.getSymbolTable().addSymbol(
13✔
265
                        node.exceptionVariableExpression.getName(),
266
                        { definingNode: node, isInstance: true },
267
                        //TODO I think we can produce a slightly more specific type here (like an AA but with the known exception properties)
268
                        DynamicType.instance,
269
                        SymbolTypeFlag.runtime
270
                    );
271
                    //brighterscript allows catch without an exception variable
272
                } else if (isBrighterscript && !node.exceptionVariableExpression) {
2!
273
                    //this is fine
274

275
                    //brighterscript allows a typecast expression here
276
                } else if (isBrighterscript && isTypecastExpression(node.exceptionVariableExpression) && isVariableExpression(node.exceptionVariableExpression.obj)) {
×
277
                    node.parent.getSymbolTable().addSymbol(
×
278
                        node.exceptionVariableExpression.obj.getName(),
279
                        { definingNode: node, isInstance: true },
280
                        node.exceptionVariableExpression.getType({ flags: SymbolTypeFlag.runtime }),
281
                        SymbolTypeFlag.runtime
282
                    );
283

284
                    //no other expressions are allowed here
285
                } else {
286
                    this.event.program.diagnostics.register({
×
287
                        ...DiagnosticMessages.expectedExceptionVarToFollowCatch(),
288
                        location: node.exceptionVariableExpression?.location ?? node.tokens.catch?.location
×
289
                    });
290
                }
291
            },
292
            DimStatement: (node) => {
293
                if (node.tokens.name) {
18!
294
                    node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true }, node.getType({ flags: SymbolTypeFlag.runtime }), SymbolTypeFlag.runtime);
18✔
295
                }
296
            },
297
            ReturnStatement: (node) => {
298
                const func = node.findAncestor<FunctionExpression>(isFunctionExpression);
564✔
299
                //these situations cannot have a value next to `return`
300
                if (
564✔
301
                    //`function as void`, `sub as void`
302
                    (isVariableExpression(func?.returnTypeExpression?.expression) && func.returnTypeExpression.expression.tokens.name.text?.toLowerCase() === 'void') ||
6,018!
303
                    //`sub` <without return value>
304
                    (func.tokens.functionType?.kind === TokenKind.Sub && !func.returnTypeExpression)
1,650!
305
                ) {
306
                    //there may not be a return value
307
                    if (node.value) {
26✔
308
                        this.event.program.diagnostics.register({
18✔
309
                            ...DiagnosticMessages.voidFunctionMayNotReturnValue(func.tokens.functionType?.text),
54!
310
                            location: node.location
311
                        });
312
                    }
313

314
                } else {
315
                    //there MUST be a return value
316
                    if (!node.value) {
538✔
317
                        this.event.program.diagnostics.register({
21✔
318
                            ...DiagnosticMessages.nonVoidFunctionMustReturnValue(func?.tokens.functionType?.text),
126!
319
                            location: node.location
320
                        });
321
                    }
322
                }
323
            },
324
            ContinueStatement: (node) => {
325
                this.validateContinueStatement(node);
8✔
326
            },
327
            TypecastStatement: (node) => {
328
                const obj = node.typecastExpression.obj;
30✔
329
                if (isVariableExpression(obj)) {
30✔
330
                    node.parent.getSymbolTable().addSymbol(obj.tokens.name.text, { definingNode: node, doNotMerge: true, isInstance: true }, node.getType({ flags: SymbolTypeFlag.typetime }), SymbolTypeFlag.runtime);
28✔
331
                }
332
            },
333
            ConditionalCompileConstStatement: (node) => {
334
                const assign = node.assignment;
10✔
335
                const constNameLower = assign.tokens.name?.text.toLowerCase();
10!
336
                const astBsConsts = this.event.file.ast.bsConsts;
10✔
337
                if (isLiteralExpression(assign.value)) {
10!
338
                    astBsConsts.set(constNameLower, assign.value.tokens.value.text.toLowerCase() === 'true');
10✔
339
                } else if (isVariableExpression(assign.value)) {
×
340
                    if (this.validateConditionalCompileConst(assign.value.tokens.name)) {
×
341
                        astBsConsts.set(constNameLower, astBsConsts.get(assign.value.tokens.name.text.toLowerCase()));
×
342
                    }
343
                }
344
            },
345
            ConditionalCompileStatement: (node) => {
346
                this.validateConditionalCompileConst(node.tokens.condition);
24✔
347
            },
348
            ConditionalCompileErrorStatement: (node) => {
349
                this.event.program.diagnostics.register({
1✔
350
                    ...DiagnosticMessages.hashError(node.tokens.message.text),
351
                    location: node.location
352
                });
353
            },
354
            AliasStatement: (node) => {
355
                // eslint-disable-next-line no-bitwise
356
                const targetType = node.value.getType({ flags: SymbolTypeFlag.typetime | SymbolTypeFlag.runtime });
30✔
357

358
                // eslint-disable-next-line no-bitwise
359
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, doNotMerge: true, isAlias: true }, targetType, SymbolTypeFlag.runtime | SymbolTypeFlag.typetime);
30✔
360

361
            },
362
            TypeStatement: (node) => {
363
                this.validateDeclarationLocations(node, 'type', () => util.createBoundingRange(node.tokens.type, node.tokens.name));
46✔
364
                const wrappedNodeType = node.getType({ flags: SymbolTypeFlag.runtime });
46✔
365
                const typeStmtType = new TypeStatementType(node.tokens.name.text, wrappedNodeType);
46✔
366
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, isFromTypeStatement: true }, typeStmtType, SymbolTypeFlag.typetime);
46✔
367

368
            },
369
            IfStatement: (node) => {
370
                this.setUpComplementSymbolTables(node, isIfStatement);
157✔
371
            },
372
            Block: (node) => {
373
                const blockSymbolTable = node.symbolTable;
3,108✔
374
                if (node.findAncestor<Block>(isFunctionExpression)) {
3,108✔
375
                    // this block is in a function. order matters!
376
                    blockSymbolTable.isOrdered = true;
3,104✔
377
                }
378
                if (!isFunctionExpression(node.parent) && node.parent) {
3,108✔
379
                    node.symbolTable.name = `Block-${node.parent.kind}@${node.location?.range?.start?.line}`;
339✔
380
                    // we're a block inside another block (or body). This block is a pocket in the bigger block
381
                    node.parent.getSymbolTable().addPocketTable({
339✔
382
                        index: node.parent.statementIndex,
383
                        table: blockSymbolTable,
384
                        // code always flows through ConditionalCompiles, because we walk according to defined BSConsts
385
                        willAlwaysBeExecuted: isConditionalCompileStatement(node.parent)
386
                    });
387

388
                    if (isForStatement(node.parent)) {
339✔
389
                        const counterDecl = node.parent.counterDeclaration;
27✔
390
                        const loopVar = counterDecl.tokens.name;
27✔
391
                        const loopVarType = counterDecl.getType({ flags: SymbolTypeFlag.runtime });
27✔
392
                        blockSymbolTable.addSymbol(loopVar.text, { isInstance: true }, loopVarType, SymbolTypeFlag.runtime);
27✔
393

394
                    } else if (isForEachStatement(node.parent)) {
312✔
395
                        const loopVarType = node.parent.getLoopVariableType({ flags: SymbolTypeFlag.runtime });
58✔
396
                        blockSymbolTable.addSymbol(node.parent.tokens.item.text, { isInstance: true }, loopVarType, SymbolTypeFlag.runtime);
58✔
397
                    }
398
                }
399
            },
400
            VariableExpression: (node) => {
401
                //flag reserved unreferencable builtins (e.g. `ObjFun`, `type`) used in non-call position.
402
                //these compile cleanly as values today but are device compile errors
403
                //(`Syntax Error. Builtin function call expected`).
404
                const name = node.tokens.name?.text;
5,954!
405
                if (
5,954✔
406
                    name &&
12,069✔
407
                    UnreferencableBuiltins.has(name.toLowerCase()) &&
408
                    //only valid use is as the callee of a CallExpression
409
                    !(isCallExpression(node.parent) && node.parent.callee === node)
309✔
410
                ) {
411
                    this.event.program.diagnostics.register({
15✔
412
                        ...DiagnosticMessages.reservedBuiltinUsedAsValue(name),
413
                        location: node.tokens.name.location
414
                    });
415
                }
416
            },
417
            AstNode: (node) => {
418
                //check for doc comments
419
                if (!node.leadingTrivia || node.leadingTrivia.length === 0) {
33,368✔
420
                    return;
6,187✔
421
                }
422
                const doc = brsDocParser.parseNode(node);
27,181✔
423
                if (doc.tags.length === 0) {
27,181✔
424
                    return;
27,129✔
425
                }
426

427
                let funcExpr = node.findAncestor<FunctionExpression>(isFunctionExpression);
52✔
428
                if (funcExpr) {
52✔
429
                    // handle comment tags inside a function expression
430
                    this.processDocTagsInFunction(doc, node, funcExpr);
8✔
431
                } else {
432
                    //handle comment tags outside of a function expression
433
                    this.processDocTagsAtTopLevel(doc, node);
44✔
434
                }
435
            }
436
        });
437

438
        this.event.file.ast.walk((node, parent) => {
2,479✔
439
            visitor(node, parent);
33,368✔
440
        }, {
441
            walkMode: WalkMode.visitAllRecursive
442
        });
443
    }
444

445
    private processDocTagsInFunction(doc: BrightScriptDoc, node: AstNode, funcExpr: FunctionExpression) {
446
        //TODO: Handle doc tags that influence the function they're in
447

448
        // For example, declaring variable types:
449
        // const symbolTable = funcExpr.body.getSymbolTable();
450

451
        // for (const varTag of doc.getAllTags(BrsDocTagKind.Var)) {
452
        //     const varName = (varTag as BrsDocParamTag).name;
453
        //     const varTypeStr = (varTag as BrsDocParamTag).type;
454
        //     const data: ExtraSymbolData = {};
455
        //     const type = doc.getTypeFromContext(varTypeStr, node, { flags: SymbolTypeFlag.typetime, fullName: varTypeStr, data: data, tableProvider: () => symbolTable });
456
        //     if (type) {
457
        //         symbolTable.addSymbol(varName, { ...data, isFromDocComment: true }, type, SymbolTypeFlag.runtime);
458
        //     }
459
        // }
460
    }
461

462
    private processDocTagsAtTopLevel(doc: BrightScriptDoc, node: AstNode) {
463
        //TODO:
464
        // - handle import statements?
465
        // - handle library statements?
466
        // - handle typecast statements?
467
        // - handle alias statements?
468
        // - handle const statements?
469
        // - allow interface definitions?
470
    }
471

472
    /**
473
     * Validate that a statement is defined in one of these specific locations
474
     *  - the root of the AST
475
     *  - inside a namespace
476
     * This is applicable to things like FunctionStatement, ClassStatement, NamespaceStatement, EnumStatement, InterfaceStatement
477
     */
478
    private validateDeclarationLocations(statement: Statement, keyword: string, rangeFactory?: () => (Range | undefined)) {
479
        //if nested inside a namespace, or defined at the root of the AST (i.e. in a body that has no parent)
480
        const isOkDeclarationLocation = (parentNode) => {
4,206✔
481
            return isNamespaceStatement(parentNode?.parent) || (isBody(parentNode) && !parentNode?.parent);
4,211!
482
        };
483
        if (isOkDeclarationLocation(statement.parent)) {
4,206✔
484
            return;
4,187✔
485
        }
486

487
        // is this in a top levelconditional compile?
488
        if (isConditionalCompileStatement(statement.parent?.parent)) {
19!
489
            if (isOkDeclarationLocation(statement.parent.parent.parent)) {
5✔
490
                return;
4✔
491
            }
492
        }
493

494
        //the statement was defined in the wrong place. Flag it.
495
        this.event.program.diagnostics.register({
15✔
496
            ...DiagnosticMessages.keywordMustBeDeclaredAtNamespaceLevel(keyword),
497
            location: rangeFactory ? util.createLocationFromFileRange(this.event.file, rangeFactory()) : statement.location
15!
498
        });
499
    }
500

501
    private validateFunctionParameterCount(func: FunctionExpression) {
502
        if (func.parameters.length > CallExpression.MaximumArguments) {
2,769✔
503
            //flag every parameter over the limit
504
            for (let i = CallExpression.MaximumArguments; i < func.parameters.length; i++) {
2✔
505
                this.event.program.diagnostics.register({
3✔
506
                    ...DiagnosticMessages.tooManyCallableParameters(func.parameters.length, CallExpression.MaximumArguments),
507
                    location: func.parameters[i]?.tokens.name?.location ?? func.parameters[i]?.location ?? func.location
36!
508
                });
509
            }
510
        }
511
    }
512

513
    private validateEnumDeclaration(stmt: EnumStatement) {
514
        const members = stmt.getMembers();
168✔
515
        //the enum data type is based on the first member value
516
        const enumValueKind = (members.find(x => x.value)?.value as LiteralExpression)?.tokens?.value?.kind ?? TokenKind.IntegerLiteral;
229✔
517
        const memberNames = new Set<string>();
168✔
518
        for (const member of members) {
168✔
519
            const memberNameLower = member.name?.toLowerCase();
325!
520

521
            /**
522
             * flag duplicate member names
523
             */
524
            if (memberNames.has(memberNameLower)) {
325✔
525
                this.event.program.diagnostics.register({
1✔
526
                    ...DiagnosticMessages.duplicateIdentifier(member.name),
527
                    location: member.location
528
                });
529
            } else {
530
                memberNames.add(memberNameLower);
324✔
531
            }
532

533
            //Enforce all member values are the same type
534
            this.validateEnumValueTypes(member, enumValueKind);
325✔
535
        }
536
    }
537

538
    private validateEnumValueTypes(member: EnumMemberStatement, enumValueKind: TokenKind) {
539
        let memberValueKind: TokenKind;
540
        let memberValue: Expression;
541
        if (isUnaryExpression(member.value)) {
325✔
542
            memberValueKind = (member.value?.right as LiteralExpression)?.tokens?.value?.kind;
2!
543
            memberValue = member.value?.right;
2!
544
        } else {
545
            memberValueKind = (member.value as LiteralExpression)?.tokens?.value?.kind;
323✔
546
            memberValue = member.value;
323✔
547
        }
548
        const range = (memberValue ?? member)?.location?.range;
325!
549
        if (
325✔
550
            //is integer enum, has value, that value type is not integer
551
            (enumValueKind === TokenKind.IntegerLiteral && memberValueKind && memberValueKind !== enumValueKind) ||
1,086✔
552
            //has value, that value is not a literal
553
            (memberValue && !isLiteralExpression(memberValue))
554
        ) {
555
            this.event.program.diagnostics.register({
6✔
556
                ...DiagnosticMessages.enumValueMustBeType(
557
                    enumValueKind.replace(/literal$/i, '').toLowerCase()
558
                ),
559
                location: util.createLocationFromFileRange(this.event.file, range)
560
            });
561
        }
562

563
        //is non integer value
564
        if (enumValueKind !== TokenKind.IntegerLiteral) {
325✔
565
            //default value present
566
            if (memberValueKind) {
128✔
567
                //member value is same as enum
568
                if (memberValueKind !== enumValueKind) {
126✔
569
                    this.event.program.diagnostics.register({
1✔
570
                        ...DiagnosticMessages.enumValueMustBeType(
571
                            enumValueKind.replace(/literal$/i, '').toLowerCase()
572
                        ),
573
                        location: util.createLocationFromFileRange(this.event.file, range)
574
                    });
575
                }
576

577
                //default value missing
578
            } else {
579
                this.event.program.diagnostics.register({
2✔
580
                    ...DiagnosticMessages.enumValueIsRequired(
581
                        enumValueKind.replace(/literal$/i, '').toLowerCase()
582
                    ),
583
                    location: util.createLocationFromFileRange(this.event.file, range)
584
                });
585
            }
586
        }
587
    }
588

589

590
    private validateConditionalCompileConst(ccConst: Token) {
591
        const isBool = ccConst.kind === TokenKind.True || ccConst.kind === TokenKind.False;
24✔
592
        if (!isBool && !this.event.file.ast.bsConsts.has(ccConst.text.toLowerCase())) {
24✔
593
            this.event.program.diagnostics.register({
2✔
594
                ...DiagnosticMessages.hashConstDoesNotExist(),
595
                location: ccConst.location
596
            });
597
            return false;
2✔
598
        }
599
        return true;
22✔
600
    }
601

602
    /**
603
     * Find statements defined at the top level (or inside a namespace body) that are not allowed to be there
604
     */
605
    private flagTopLevelStatements() {
606
        const statements = [...this.event.file.ast.statements];
2,479✔
607
        while (statements.length > 0) {
2,479✔
608
            const statement = statements.pop();
4,468✔
609
            if (isNamespaceStatement(statement)) {
4,468✔
610
                statements.push(...statement.body.statements);
659✔
611
            } else {
612
                //only allow these statement types
613
                if (
3,809✔
614
                    !isFunctionStatement(statement) &&
8,693✔
615
                    !isClassStatement(statement) &&
616
                    !isEnumStatement(statement) &&
617
                    !isInterfaceStatement(statement) &&
618
                    !isLibraryStatement(statement) &&
619
                    !isImportStatement(statement) &&
620
                    !isConstStatement(statement) &&
621
                    !isTypecastStatement(statement) &&
622
                    !isConditionalCompileConstStatement(statement) &&
623
                    !isConditionalCompileErrorStatement(statement) &&
624
                    !isConditionalCompileStatement(statement) &&
625
                    !isAliasStatement(statement) &&
626
                    !isTypeStatement(statement)
627
                ) {
628
                    this.event.program.diagnostics.register({
10✔
629
                        ...DiagnosticMessages.unexpectedStatementOutsideFunction(),
630
                        location: statement.location
631
                    });
632
                }
633
            }
634
        }
635
    }
636

637
    private isAllowedAtTopOfFile(statement: Statement): statement is LibraryStatement | ImportStatement | TypecastStatement | AliasStatement {
638
        return isLibraryStatement(statement) || isImportStatement(statement) || isTypecastStatement(statement) || isAliasStatement(statement);
2,624✔
639
    }
640

641
    private getTopOfFileStatements() {
642
        let topOfFileIncludeStatements = [] as Array<LibraryStatement | ImportStatement | TypecastStatement | AliasStatement>;
2,478✔
643
        for (let stmt of this.event.file.parser.ast.statements) {
2,478✔
644
            //if we found a non-library statement, this statement is not at the top of the file
645
            if (this.isAllowedAtTopOfFile(stmt)) {
2,613✔
646
                topOfFileIncludeStatements.push(stmt);
257✔
647
            } else {
648
                //break out of the loop, we found all of our library statements
649
                break;
2,356✔
650
            }
651
        }
652
        return topOfFileIncludeStatements;
2,478✔
653
    }
654

655
    private validateTopOfFileStatements() {
656
        let topOfFileStatements = this.getTopOfFileStatements();
2,478✔
657

658
        let statements = [
2,478✔
659
            // eslint-disable-next-line @typescript-eslint/dot-notation
660
            ...this.event.file['_cachedLookups'].libraryStatements,
661
            // eslint-disable-next-line @typescript-eslint/dot-notation
662
            ...this.event.file['_cachedLookups'].importStatements,
663
            // eslint-disable-next-line @typescript-eslint/dot-notation
664
            ...this.event.file['_cachedLookups'].aliasStatements
665
        ];
666
        for (let result of statements) {
2,478✔
667
            //if this statement is not one of the top-of-file statements,
668
            //then add a diagnostic explaining that it is invalid
669
            if (!topOfFileStatements.includes(result)) {
249✔
670
                if (isLibraryStatement(result)) {
5✔
671
                    this.event.program.diagnostics.register({
2✔
672
                        ...DiagnosticMessages.unexpectedStatementLocation('library', 'at the top of the file'),
673
                        location: result.location
674
                    });
675
                } else if (isImportStatement(result)) {
3✔
676
                    this.event.program.diagnostics.register({
1✔
677
                        ...DiagnosticMessages.unexpectedStatementLocation('import', 'at the top of the file'),
678
                        location: result.location
679
                    });
680
                } else if (isAliasStatement(result)) {
2!
681
                    this.event.program.diagnostics.register({
2✔
682
                        ...DiagnosticMessages.unexpectedStatementLocation('alias', 'at the top of the file'),
683
                        location: result.location
684
                    });
685
                }
686
            }
687
        }
688
    }
689

690
    private validateTypecastStatements() {
691
        // eslint-disable-next-line @typescript-eslint/dot-notation
692
        for (let typecastStmt of this.event.file['_cachedLookups'].typecastStatements) {
2,478✔
693
            let isBadTypecastObj = false;
30✔
694

695
            const block = typecastStmt.findAncestor<Body | Block>(node => (isBody(node) || isBlock(node)));
30✔
696
            const resultVarStr = util.getAllDottedGetPartsAsString(typecastStmt.typecastExpression.obj);
30✔
697
            const hasFunctionAncestor = !!typecastStmt.findAncestor(isFunctionExpression);
30✔
698
            if (!isVariableExpression(typecastStmt.typecastExpression.obj)) {
30✔
699
                isBadTypecastObj = true;
2✔
700
            } else if (!hasFunctionAncestor && resultVarStr.toLowerCase() !== 'm') {
28✔
701
                // only 'm' can be typecast outside of a function body
702
                isBadTypecastObj = true;
1✔
703
            } else if (block.getSymbolTable().hasSymbol(resultVarStr, SymbolTypeFlag.typetime)) {
27✔
704
                // can only typecast runtime symbols
705
                isBadTypecastObj = true;
1✔
706
            }
707

708
            if (isBadTypecastObj) {
30✔
709
                this.event.program.diagnostics.register({
4✔
710
                    ...DiagnosticMessages.invalidTypecastStatementApplication(resultVarStr, hasFunctionAncestor),
711
                    location: typecastStmt.typecastExpression.obj.location
712
                });
713
            }
714

715
            let isFirst = true;
30✔
716
            for (let i = 0; i < typecastStmt.statementIndex; i++) {
30✔
717
                const targetStatement = block.statements[i];
11✔
718
                // allow multiple typecast statements at the top of a block or namespace, but no other statements before them
719
                isFirst = isFirst && this.isAllowedAtTopOfFile(targetStatement);
11✔
720
                if (isTypecastStatement(targetStatement) && targetStatement !== typecastStmt) {
11✔
721
                    // do not allow multiple typecast statements that typecast the same variable, even if they are at the top of the block/namespace
722
                    const otherResultVarStr = util.getAllDottedGetPartsAsString(targetStatement.typecastExpression.obj);
4✔
723
                    if (otherResultVarStr.toLowerCase() === resultVarStr.toLowerCase()) {
4✔
724
                        isFirst = false;
3✔
725
                    }
726
                }
727
                if (!isFirst) {
11✔
728
                    break;
5✔
729
                }
730
            }
731

732
            if (!isFirst) {
30✔
733
                this.event.program.diagnostics.register({
5✔
734
                    ...DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of block or namespace'),
735
                    location: typecastStmt.location
736
                });
737
            }
738
        }
739
    }
740

741
    private validateContinueStatement(statement: ContinueStatement) {
742
        const validateLoopTypeMatch = (expectedLoopType: TokenKind) => {
8✔
743
            //coerce ForEach to For
744
            expectedLoopType = expectedLoopType === TokenKind.ForEach ? TokenKind.For : expectedLoopType;
7✔
745
            const actualLoopType = statement.tokens.loopType;
7✔
746
            if (actualLoopType && expectedLoopType?.toLowerCase() !== actualLoopType.text?.toLowerCase()) {
7!
747
                this.event.program.diagnostics.register({
3✔
748
                    location: statement.tokens.loopType.location,
749
                    ...DiagnosticMessages.expectedToken(expectedLoopType)
750
                });
751
            }
752
        };
753

754
        //find the parent loop statement
755
        const parent = statement.findAncestor<WhileStatement | ForStatement | ForEachStatement>((node) => {
8✔
756
            if (isWhileStatement(node)) {
18✔
757
                validateLoopTypeMatch(node.tokens.while.kind);
3✔
758
                return true;
3✔
759
            } else if (isForStatement(node)) {
15✔
760
                validateLoopTypeMatch(node.tokens.for.kind);
3✔
761
                return true;
3✔
762
            } else if (isForEachStatement(node)) {
12✔
763
                validateLoopTypeMatch(node.tokens.forEach.kind);
1✔
764
                return true;
1✔
765
            }
766
        });
767
        //flag continue statements found outside of a loop
768
        if (!parent) {
8✔
769
            this.event.program.diagnostics.register({
1✔
770
                location: statement.location,
771
                ...DiagnosticMessages.illegalContinueStatement()
772
            });
773
        }
774
    }
775

776
    /**
777
     * Validate that there are no optional chaining operators on the left-hand-side of an assignment, indexed set, or dotted get
778
     */
779
    private validateNoOptionalChainingInVarSet(parent: AstNode, children: AstNode[]) {
780
        const nodes = [...children, parent];
127✔
781
        //flag optional chaining anywhere in the left of this statement
782
        while (nodes.length > 0) {
127✔
783
            const node = nodes.shift();
254✔
784
            if (
254✔
785
                // a?.b = true or a.b?.c = true
786
                ((isDottedSetStatement(node) || isDottedGetExpression(node)) && node.tokens.dot?.kind === TokenKind.QuestionDot) ||
1,445!
787
                // a.b?[2] = true
788
                (isIndexedGetExpression(node) && (node?.tokens.questionDot?.kind === TokenKind.QuestionDot || node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare)) ||
36!
789
                // a?[1] = true
790
                (isIndexedSetStatement(node) && node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare)
57!
791
            ) {
792
                //try to highlight the entire left-hand-side expression if possible
793
                let range: Range;
794
                if (isDottedSetStatement(parent)) {
8✔
795
                    range = util.createBoundingRange(parent.obj?.location, parent.tokens.dot, parent.tokens.name);
5!
796
                } else if (isIndexedSetStatement(parent)) {
3!
797
                    range = util.createBoundingRange(parent.obj?.location, parent.tokens.openingSquare, ...parent.indexes, parent.tokens.closingSquare);
3!
798
                } else {
799
                    range = node.location?.range;
×
800
                }
801

802
                this.event.program.diagnostics.register({
8✔
803
                    ...DiagnosticMessages.noOptionalChainingInLeftHandSideOfAssignment(),
804
                    location: util.createLocationFromFileRange(this.event.file, range)
805
                });
806
            }
807

808
            if (node === parent) {
254✔
809
                break;
127✔
810
            } else {
811
                nodes.push(node.parent);
127✔
812
            }
813
        }
814
    }
815

816
    private setUpComplementSymbolTables(node: IfStatement | ConditionalCompileStatement, predicate: (node: AstNode) => boolean) {
817
        if (isBlock(node.elseBranch)) {
157✔
818
            const elseTable = node.elseBranch.symbolTable;
28✔
819
            let currentNode = node;
28✔
820
            while (predicate(currentNode)) {
28✔
821
                const thenBranch = (currentNode as IfStatement | ConditionalCompileStatement).thenBranch;
39✔
822
                elseTable.complementOtherTable(thenBranch.symbolTable);
39✔
823
                currentNode = currentNode.parent as IfStatement | ConditionalCompileStatement;
39✔
824
            }
825
        }
826
    }
827

828
    /**
829
     * Add a diagnostic if the configured minFirmwareVersion is lower than the version that
830
     * introduced optional chaining support (Roku OS 11).
831
     * This applies to both .brs and .bs files because optional chaining is not transpiled —
832
     * it is emitted as-is, so the target device must natively support it.
833
     */
834
    private validateMinFirmwareVersionForOptionalChaining(range: Range | undefined) {
835
        const minFirmwareVersion = this.event.program.getMinFirmwareVersion();
37✔
836
        if (semver.lt(minFirmwareVersion, OPTIONAL_CHAINING_MIN_FIRMWARE_VERSION)) {
37✔
837
            this.event.program.diagnostics.register({
4✔
838
                ...DiagnosticMessages.featureRequiresMinFirmwareVersion(
839
                    'optional chaining',
840
                    OPTIONAL_CHAINING_MIN_FIRMWARE_VERSION,
841
                    minFirmwareVersion
842
                ),
843
                location: util.createLocationFromFileRange(this.event.file, range)
844
            });
845
        }
846
    }
847

848
    /**
849
     * For a bare top-level call to a known global callable, fire one deprecation/removal
850
     * diagnostic driven by `callable.availability`. The rsg axis takes precedence: if it
851
     * fires, the os axis is skipped entirely. The os axis is only consulted when rsg is
852
     * silent (rsg axis not configured, or effective rsg below its thresholds).
853
     *
854
     * Skips method calls (`m.foo()`) and namespaced calls (`alpha.foo()`) — only the bare
855
     * top-level builtin form resolves to a global callable.
856
     */
857
    private validateGlobalCallableAvailability(node: CallExpression) {
858
        if (!isVariableExpression(node.callee)) {
1,257✔
859
            return;
455✔
860
        }
861
        const calleeName = node.callee.name?.text;
802!
862
        if (!calleeName) {
802!
NEW
863
            return;
×
864
        }
865
        const callable = globalCallableMap.get(calleeName.toLowerCase());
802✔
866
        const availability = callable?.availability;
802✔
867
        if (!availability) {
802✔
868
            return;
793✔
869
        }
870
        const rsgDiagnostic = this.computeAvailabilityDiagnostic(calleeName, 'rsg', availability.rsg, this.event.file.program.getRsgVersion());
9✔
871
        const diagnostic = rsgDiagnostic ??
9✔
872
            this.computeAvailabilityDiagnostic(calleeName, 'os', availability.os, this.event.file.program.getMinFirmwareVersion());
873
        if (diagnostic) {
9✔
874
            this.event.program.diagnostics.register({
8✔
875
                ...diagnostic,
876
                location: node.callee.location
877
            });
878
        }
879
    }
880

881
    /**
882
     * Compute (but don't emit) the diagnostic for one axis of {@link Availability}: returns
883
     * `globalCallableRemoved` if the project's effective version is at/past the axis's
884
     * `removed` threshold, otherwise `globalCallableDeprecated` if it's at/past `deprecated`,
885
     * otherwise `undefined`.
886
     *
887
     * `effectiveVersion` is expected in canonical semver form (program getters guarantee this);
888
     * availability constants are authored in canonical form too, so no coercion is needed here.
889
     */
890
    private computeAvailabilityDiagnostic(calleeName: string, axis: AvailabilityAxis, info: { added?: string; deprecated?: string; removed?: string } | undefined, effectiveVersion: string) {
891
        if (!info) {
11!
NEW
892
            return undefined;
×
893
        }
894
        if (info.removed && semver.gte(effectiveVersion, info.removed)) {
11✔
895
            return DiagnosticMessages.globalCallableRemoved(calleeName, axis, info.removed, effectiveVersion);
7✔
896
        }
897
        if (info.deprecated && semver.gte(effectiveVersion, info.deprecated)) {
4✔
898
            return DiagnosticMessages.globalCallableDeprecated(calleeName, axis, info.deprecated, effectiveVersion);
1✔
899
        }
900
        return undefined;
3✔
901
    }
902
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc