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

rokucommunity / brighterscript / #15512

28 Mar 2026 11:39PM UTC coverage: 88.976% (-0.06%) from 89.035%
#15512

push

web-flow
Merge f9c27a6f4 into f3673e7df

8184 of 9705 branches covered (84.33%)

Branch coverage included in aggregate %.

105 of 111 new or added lines in 7 files covered. (94.59%)

10428 of 11213 relevant lines covered (93.0%)

1991.68 hits per line

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

90.23
/src/bscPlugin/validation/BrsFileValidator.ts
1
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';
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 { OnFileValidateEvent } from '../../interfaces';
6
import { TokenKind } 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, WhileStatement } from '../../parser/Statement';
11
import { DynamicType } from '../../types/DynamicType';
1✔
12
import { InterfaceType } from '../../types/InterfaceType';
1✔
13
import util from '../../util';
1✔
14
import type { Range } from 'vscode-languageserver';
15

16
export class BrsFileValidator {
1✔
17
    constructor(
18
        public event: OnFileValidateEvent<BrsFile>
948✔
19
    ) {
20
    }
21

22
    public process() {
23
        util.validateTooDeepFile(this.event.file);
948✔
24
        this.walk();
948✔
25
        this.flagTopLevelStatements();
948✔
26
        //only validate the file if it was actually parsed (skip files containing typedefs)
27
        if (!this.event.file.hasTypedef) {
948✔
28
            this.validateImportStatements();
947✔
29
        }
30
    }
31

32
    /**
33
     * Walk the full AST
34
     */
35
    private walk() {
36
        const visitor = createVisitor({
948✔
37
            MethodStatement: (node) => {
38
                //add the `super` symbol to class methods
39
                node.func.body.symbolTable.addSymbol('super', undefined, DynamicType.instance);
137✔
40
            },
41
            CallfuncExpression: (node) => {
42
                if (node.args.length > 5) {
14✔
43
                    this.event.file.addDiagnostic({
1✔
44
                        ...DiagnosticMessages.callfuncHasToManyArgs(node.args.length),
45
                        range: node.methodName.range
46
                    });
47
                }
48
            },
49
            EnumStatement: (node) => {
50
                this.validateDeclarationLocations(node, 'enum', () => util.createBoundingRange(node.tokens.enum, node.tokens.name));
84✔
51

52
                this.validateEnumDeclaration(node);
84✔
53

54
                //register this enum declaration
55
                if (node.tokens.name) {
84!
56
                    node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, node.tokens.name.range, DynamicType.instance);
84!
57
                }
58
            },
59
            ClassStatement: (node) => {
60
                this.validateDeclarationLocations(node, 'class', () => util.createBoundingRange(node.classKeyword, node.name));
205✔
61

62
                //register this class
63
                if (node.name) {
205✔
64
                    node.parent.getSymbolTable()?.addSymbol(node.name.text, node.name.range, DynamicType.instance);
204!
65
                }
66
            },
67
            AssignmentStatement: (node) => {
68
                //register this variable
69
                if (node.name) {
399!
70
                    node.parent.getSymbolTable()?.addSymbol(node.name.text, node.name.range, DynamicType.instance);
399!
71
                }
72
            },
73
            DottedSetStatement: (node) => {
74
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
22✔
75
            },
76
            IndexedSetStatement: (node) => {
77
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
20✔
78
            },
79
            ForEachStatement: (node) => {
80
                //register the for loop variable
81
                node.parent.getSymbolTable()?.addSymbol(node.item.text, node.item.range, DynamicType.instance);
9!
82
            },
83
            NamespaceStatement: (node) => {
84
                this.validateDeclarationLocations(node, 'namespace', () => util.createBoundingRange(node.keyword, node.nameExpression));
195✔
85

86
                if (node.name) {
195!
87
                    node.parent.getSymbolTable().addSymbol(
195✔
88
                        node.name.split('.')[0],
89
                        node.nameExpression.range,
90
                        DynamicType.instance
91
                    );
92
                }
93
            },
94
            FunctionStatement: (node) => {
95
                this.validateDeclarationLocations(node, 'function', () => util.createBoundingRange(node.func.functionType, node.name));
881✔
96
                if (node.name?.text) {
881✔
97
                    node.parent.getSymbolTable().addSymbol(
880✔
98
                        node.name.text,
99
                        node.name.range,
100
                        DynamicType.instance
101
                    );
102
                }
103

104
                const namespace = node.findAncestor(isNamespaceStatement);
881✔
105
                //this function is declared inside a namespace
106
                if (namespace) {
881✔
107
                    //add the transpiled name for namespaced functions to the root symbol table
108
                    const transpiledNamespaceFunctionName = node.getName(ParseMode.BrightScript);
84✔
109
                    const funcType = node.func.getFunctionType();
84✔
110
                    funcType.setName(transpiledNamespaceFunctionName);
84✔
111

112
                    if (node.name) {
84✔
113
                        this.event.file.parser.ast.symbolTable.addSymbol(
83✔
114
                            transpiledNamespaceFunctionName,
115
                            node.name.range,
116
                            funcType
117
                        );
118
                    }
119
                }
120
            },
121
            FunctionExpression: (node) => {
122
                if (!node.symbolTable.hasSymbol('m')) {
1,038✔
123
                    node.symbolTable.addSymbol('m', undefined, DynamicType.instance);
1,016✔
124
                }
125
                this.validateFunctionParameterCount(node);
1,038✔
126
                this.validateFunctionParameterNames(node);
1,038✔
127
            },
128
            FunctionParameterExpression: (node) => {
129
                if (node.name) {
564!
130
                    const paramName = node.name.text;
564✔
131
                    const symbolTable = node.getSymbolTable();
564✔
132
                    symbolTable?.addSymbol(paramName, node.name.range, node.type);
564!
133
                }
134
            },
135
            InterfaceStatement: (node) => {
136
                this.validateDeclarationLocations(node, 'interface', () => util.createBoundingRange(node.tokens.interface, node.tokens.name));
38✔
137
                if (node.tokens.name) {
38!
138
                    node.parent?.getSymbolTable()?.addSymbol(node.tokens.name.text, node.tokens.name.range, new InterfaceType(new Map()));
38!
139
                }
140
            },
141
            ConstStatement: (node) => {
142
                this.validateDeclarationLocations(node, 'const', () => util.createBoundingRange(node.tokens.const, node.tokens.name));
77✔
143
                if (node.tokens.name) {
77!
144
                    node.parent.getSymbolTable().addSymbol(node.tokens.name.text, node.tokens.name.range, DynamicType.instance);
77✔
145
                }
146
            },
147
            CatchStatement: (node) => {
148
                node.parent.getSymbolTable().addSymbol(node.exceptionVariable.text, node.exceptionVariable.range, DynamicType.instance);
4✔
149
            },
150
            DimStatement: (node) => {
151
                if (node.identifier) {
18!
152
                    node.parent.getSymbolTable().addSymbol(node.identifier.text, node.identifier.range, DynamicType.instance);
18✔
153
                }
154
            },
155
            ReturnStatement: (node) => {
156
                const func = node.findAncestor<FunctionExpression>(isFunctionExpression);
129✔
157
                //these situations cannot have a value next to `return`
158
                if (
129✔
159
                    //`function as void`, `sub as void`
160
                    func?.returnTypeToken?.kind === TokenKind.Void ||
1,048!
161
                    //`sub` <without return value>
162
                    (func.functionType?.kind === TokenKind.Sub && !func.returnTypeToken)
366!
163
                ) {
164
                    //there may not be a return value
165
                    if (node.value) {
16✔
166
                        this.event.file.addDiagnostic({
15✔
167
                            ...DiagnosticMessages.voidFunctionMayNotReturnValue(func.functionType?.text),
45!
168
                            range: node.range
169
                        });
170
                    }
171

172
                } else {
173
                    //there MUST be a return value
174
                    if (!node.value) {
113✔
175
                        this.event.file.addDiagnostic({
18✔
176
                            ...DiagnosticMessages.nonVoidFunctionMustReturnValue(func?.functionType?.text),
108!
177
                            range: node.range
178
                        });
179
                    }
180
                }
181
            },
182
            ContinueStatement: (node) => {
183
                this.validateContinueStatement(node);
8✔
184
            }
185
        });
186

187
        this.event.file.ast.walk((node, parent) => {
948✔
188
            visitor(node, parent);
10,950✔
189
        }, {
190
            walkMode: WalkMode.visitAllRecursive
191
        });
192
    }
193

194
    /**
195
     * Validate that a statement is defined in one of these specific locations
196
     *  - the root of the AST
197
     *  - inside a namespace
198
     * This is applicable to things like FunctionStatement, ClassStatement, NamespaceStatement, EnumStatement, InterfaceStatement
199
     */
200
    private validateDeclarationLocations(statement: Statement, keyword: string, rangeFactory?: () => (Range | undefined)) {
201
        //if nested inside a namespace, or defined at the root of the AST (i.e. in a body that has no parent)
202
        if (isNamespaceStatement(statement.parent?.parent) || (isBody(statement.parent) && !statement.parent?.parent)) {
1,480!
203
            return;
1,467✔
204
        }
205
        //the statement was defined in the wrong place. Flag it.
206
        this.event.file.addDiagnostic({
13✔
207
            ...DiagnosticMessages.keywordMustBeDeclaredAtNamespaceLevel(keyword),
208
            range: rangeFactory?.() ?? statement.range
78!
209
        });
210
    }
211

212
    private validateFunctionParameterCount(func: FunctionExpression) {
213
        if (func.parameters.length > CallExpression.MaximumArguments) {
1,038✔
214
            //flag every parameter over the limit
215
            for (let i = CallExpression.MaximumArguments; i < func.parameters.length; i++) {
2✔
216
                this.event.file.addDiagnostic({
3✔
217
                    ...DiagnosticMessages.tooManyCallableParameters(func.parameters.length, CallExpression.MaximumArguments),
218
                    range: func.parameters[i].name.range
219
                });
220
            }
221
        }
222
    }
223

224
    private validateFunctionParameterNames(func: FunctionExpression) {
225
        const seen = new Set<string>();
1,038✔
226
        for (const param of func.parameters) {
1,038✔
227
            const nameLower = param.name?.text?.toLowerCase();
564!
228
            if (!nameLower) {
564!
NEW
229
                continue;
×
230
            }
231
            if (seen.has(nameLower)) {
564✔
232
                this.event.file.addDiagnostic({
3✔
233
                    ...DiagnosticMessages.duplicateFunctionParameter(param.name.text),
234
                    range: param.name.range
235
                });
236
            } else {
237
                seen.add(nameLower);
561✔
238
            }
239
        }
240
    }
241

242
    private validateEnumDeclaration(stmt: EnumStatement) {
243
        const members = stmt.getMembers();
84✔
244
        //the enum data type is based on the first member value
245
        const enumValueKind = (members.find(x => x.value)?.value as LiteralExpression)?.token?.kind ?? TokenKind.IntegerLiteral;
101✔
246
        const memberNames = new Set<string>();
84✔
247
        for (const member of members) {
84✔
248
            const memberNameLower = member.name?.toLowerCase();
136!
249

250
            /**
251
             * flag duplicate member names
252
             */
253
            if (memberNames.has(memberNameLower)) {
136✔
254
                this.event.file.addDiagnostic({
1✔
255
                    ...DiagnosticMessages.duplicateIdentifier(member.name),
256
                    range: member.range
257
                });
258
            } else {
259
                memberNames.add(memberNameLower);
135✔
260
            }
261

262
            //Enforce all member values are the same type
263
            this.validateEnumValueTypes(member, enumValueKind);
136✔
264
        }
265
    }
266

267
    private validateEnumValueTypes(member: EnumMemberStatement, enumValueKind: TokenKind) {
268
        let memberValueKind: TokenKind;
269
        let memberValue: Expression;
270
        if (isUnaryExpression(member.value)) {
136✔
271
            memberValueKind = (member.value?.right as LiteralExpression)?.token?.kind;
2!
272
            memberValue = member.value?.right;
2!
273
        } else {
274
            memberValueKind = (member.value as LiteralExpression)?.token?.kind;
134✔
275
            memberValue = member.value;
134✔
276
        }
277
        const range = (memberValue ?? member)?.range;
136!
278
        if (
136✔
279
            //is integer enum, has value, that value type is not integer
280
            (enumValueKind === TokenKind.IntegerLiteral && memberValueKind && memberValueKind !== enumValueKind) ||
446✔
281
            //has value, that value is not a literal
282
            (memberValue && !isLiteralExpression(memberValue))
283
        ) {
284
            this.event.file.addDiagnostic({
5✔
285
                ...DiagnosticMessages.enumValueMustBeType(
286
                    enumValueKind.replace(/literal$/i, '').toLowerCase()
287
                ),
288
                range: range
289
            });
290
        }
291

292
        //is non integer value
293
        if (enumValueKind !== TokenKind.IntegerLiteral) {
136✔
294
            //default value present
295
            if (memberValueKind) {
62✔
296
                //member value is same as enum
297
                if (memberValueKind !== enumValueKind) {
60✔
298
                    this.event.file.addDiagnostic({
1✔
299
                        ...DiagnosticMessages.enumValueMustBeType(
300
                            enumValueKind.replace(/literal$/i, '').toLowerCase()
301
                        ),
302
                        range: range
303
                    });
304
                }
305

306
                //default value missing
307
            } else {
308
                this.event.file.addDiagnostic({
2✔
309
                    file: this.event.file,
310
                    ...DiagnosticMessages.enumValueIsRequired(
311
                        enumValueKind.replace(/literal$/i, '').toLowerCase()
312
                    ),
313
                    range: range
314
                });
315
            }
316
        }
317
    }
318

319
    /**
320
     * Find statements defined at the top level (or inside a namespace body) that are not allowed to be there
321
     */
322
    private flagTopLevelStatements() {
323
        const statements = [...this.event.file.ast.statements];
948✔
324
        while (statements.length > 0) {
948✔
325
            const statement = statements.pop();
1,524✔
326
            if (isNamespaceStatement(statement)) {
1,524✔
327
                statements.push(...statement.body.statements);
192✔
328
            } else {
329
                //only allow these statement types
330
                if (
1,332✔
331
                    !isFunctionStatement(statement) &&
2,700✔
332
                    !isClassStatement(statement) &&
333
                    !isEnumStatement(statement) &&
334
                    !isInterfaceStatement(statement) &&
335
                    !isCommentStatement(statement) &&
336
                    !isLibraryStatement(statement) &&
337
                    !isImportStatement(statement) &&
338
                    !isConstStatement(statement) &&
339
                    !isTypecastStatement(statement) &&
340
                    !isAliasStatement(statement) &&
341
                    !isTypeStatement(statement)
342
                ) {
343
                    this.event.file.addDiagnostic({
8✔
344
                        ...DiagnosticMessages.unexpectedStatementOutsideFunction(),
345
                        range: statement.range
346
                    });
347
                }
348
            }
349
        }
350
    }
351

352
    private validateImportStatements() {
353
        let topOfFileIncludeStatements = [] as Array<LibraryStatement | ImportStatement>;
947✔
354
        for (let stmt of this.event.file.parser.ast.statements) {
947✔
355
            //skip comments
356
            if (isCommentStatement(stmt)) {
904✔
357
                continue;
7✔
358
            }
359
            //if we found a non-library statement, this statement is not at the top of the file
360
            if (isLibraryStatement(stmt) || isImportStatement(stmt)) {
897✔
361
                topOfFileIncludeStatements.push(stmt);
26✔
362
            } else {
363
                //break out of the loop, we found all of our library statements
364
                break;
871✔
365
            }
366
        }
367

368
        let statements = [
947✔
369
            // eslint-disable-next-line @typescript-eslint/dot-notation
370
            ...this.event.file['_parser'].references.libraryStatements,
371
            // eslint-disable-next-line @typescript-eslint/dot-notation
372
            ...this.event.file['_parser'].references.importStatements
373
        ];
374
        for (let result of statements) {
947✔
375
            //if this statement is not one of the top-of-file statements,
376
            //then add a diagnostic explaining that it is invalid
377
            if (!topOfFileIncludeStatements.includes(result)) {
29✔
378
                if (isLibraryStatement(result)) {
3✔
379
                    this.event.file.diagnostics.push({
2✔
380
                        ...DiagnosticMessages.libraryStatementMustBeDeclaredAtTopOfFile(),
381
                        range: result.range,
382
                        file: this.event.file
383
                    });
384
                } else if (isImportStatement(result)) {
1!
385
                    this.event.file.diagnostics.push({
1✔
386
                        ...DiagnosticMessages.importStatementMustBeDeclaredAtTopOfFile(),
387
                        range: result.range,
388
                        file: this.event.file
389
                    });
390
                }
391
            }
392
        }
393
    }
394

395
    private validateContinueStatement(statement: ContinueStatement) {
396
        const validateLoopTypeMatch = (expectedLoopType: TokenKind) => {
8✔
397
            //coerce ForEach to For
398
            expectedLoopType = expectedLoopType === TokenKind.ForEach ? TokenKind.For : expectedLoopType;
7✔
399
            const actualLoopType = statement.tokens.loopType;
7✔
400
            if (actualLoopType && expectedLoopType?.toLowerCase() !== actualLoopType.text?.toLowerCase()) {
7!
401
                this.event.file.addDiagnostic({
3✔
402
                    range: statement.tokens.loopType.range,
403
                    ...DiagnosticMessages.expectedToken(expectedLoopType)
404
                });
405
            }
406
        };
407

408
        //find the parent loop statement
409
        const parent = statement.findAncestor<WhileStatement | ForStatement | ForEachStatement>((node) => {
8✔
410
            if (isWhileStatement(node)) {
18✔
411
                validateLoopTypeMatch(node.tokens.while.kind);
3✔
412
                return true;
3✔
413
            } else if (isForStatement(node)) {
15✔
414
                validateLoopTypeMatch(node.forToken.kind);
3✔
415
                return true;
3✔
416
            } else if (isForEachStatement(node)) {
12✔
417
                validateLoopTypeMatch(node.tokens.forEach.kind);
1✔
418
                return true;
1✔
419
            }
420
        });
421
        //flag continue statements found outside of a loop
422
        if (!parent) {
8✔
423
            this.event.file.addDiagnostic({
1✔
424
                range: statement.range,
425
                ...DiagnosticMessages.illegalContinueStatement()
426
            });
427
        }
428
    }
429

430
    /**
431
     * Validate that there are no optional chaining operators on the left-hand-side of an assignment, indexed set, or dotted get
432
     */
433
    private validateNoOptionalChainingInVarSet(parent: AstNode, children: AstNode[]) {
434
        const nodes = [...children, parent];
42✔
435
        //flag optional chaining anywhere in the left of this statement
436
        while (nodes.length > 0) {
42✔
437
            const node = nodes.shift();
84✔
438
            if (
84✔
439
                // a?.b = true or a.b?.c = true
440
                ((isDottedSetStatement(node) || isDottedGetExpression(node)) && node.dot?.kind === TokenKind.QuestionDot) ||
458!
441
                // a.b?[2] = true
442
                (isIndexedGetExpression(node) && (node?.questionDotToken?.kind === TokenKind.QuestionDot || node.openingSquare?.kind === TokenKind.QuestionLeftSquare)) ||
27!
443
                // a?[1] = true
444
                (isIndexedSetStatement(node) && node.openingSquare?.kind === TokenKind.QuestionLeftSquare)
60!
445
            ) {
446
                //try to highlight the entire left-hand-side expression if possible
447
                let range: Range;
448
                if (isDottedSetStatement(parent)) {
8✔
449
                    range = util.createBoundingRange(parent.obj, parent.dot, parent.name);
5✔
450
                } else if (isIndexedSetStatement(parent)) {
3!
451
                    range = util.createBoundingRange(parent.obj, parent.openingSquare, parent.index, parent.closingSquare);
3✔
452
                } else {
453
                    range = node.range;
×
454
                }
455

456
                this.event.file.addDiagnostic({
8✔
457
                    ...DiagnosticMessages.noOptionalChainingInLeftHandSideOfAssignment(),
458
                    range: range
459
                });
460
            }
461

462
            if (node === parent) {
84✔
463
                break;
42✔
464
            } else {
465
                nodes.push(node.parent);
42✔
466
            }
467
        }
468
    }
469
}
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