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

rokucommunity / brighterscript / #13603

13 Jan 2025 03:29PM UTC coverage: 86.902%. Remained the same
#13603

push

web-flow
Merge 6255e8be5 into 9d6ef67ba

12080 of 14675 branches covered (82.32%)

Branch coverage included in aggregate %.

94 of 100 new or added lines in 13 files covered. (94.0%)

103 existing lines in 10 files now uncovered.

13052 of 14245 relevant lines covered (91.63%)

31872.5 hits per line

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

86.84
/src/bscPlugin/validation/BrsFileValidator.ts
1
import { isAliasStatement, isArrayType, isBlock, isBody, isCallableType, isClassStatement, isClassType, isConditionalCompileConstStatement, isConditionalCompileErrorStatement, isConditionalCompileStatement, isConstStatement, isDottedGetExpression, isDottedSetStatement, isEnumStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isImportStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceStatement, isInvalidType, isLibraryStatement, isLiteralExpression, isMethodStatement, isNamespaceStatement, isStatement, isTypecastExpression, isTypecastStatement, isTypedFunctionType, 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, OnFileValidateEvent, TypeCompatibilityData } from '../../interfaces';
6
import { TokenKind } from '../../lexer/TokenKind';
1✔
7
import type { AstNode, Expression, Statement } from '../../parser/AstNode';
8
import type { FunctionExpression, LiteralExpression } from '../../parser/Expression';
9
import { CallExpression } from '../../parser/Expression';
1✔
10
import { ParseMode } from '../../parser/Parser';
1✔
11
import type { ContinueStatement, EnumMemberStatement, EnumStatement, ForEachStatement, ForStatement, ImportStatement, LibraryStatement, Body, WhileStatement, TypecastStatement, Block, AliasStatement } from '../../parser/Statement';
12
import { SymbolTypeFlag } from '../../SymbolTypeFlag';
1✔
13
import { ArrayDefaultTypeReferenceType } from '../../types/ReferenceType';
1✔
14
import { AssociativeArrayType } from '../../types/AssociativeArrayType';
1✔
15
import { DynamicType } from '../../types/DynamicType';
1✔
16
import util from '../../util';
1✔
17
import type { Range } from 'vscode-languageserver';
18
import type { Token } from '../../lexer/Token';
19
import type { BrightScriptDoc } from '../../parser/BrightScriptDocParser';
20
import brsDocParser from '../../parser/BrightScriptDocParser';
1✔
21
import { UninitializedType } from '../../types';
1✔
22

23
export class BrsFileValidator {
1✔
24
    constructor(
25
        public event: OnFileValidateEvent<BrsFile>
1,694✔
26
    ) {
27
    }
28

29

30
    public process() {
31
        const unlinkGlobalSymbolTable = this.event.file.parser.symbolTable.pushParentProvider(() => this.event.program.globalScope.symbolTable);
6,620✔
32

33
        util.validateTooDeepFile(this.event.file);
1,694✔
34

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

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

43
        this.walk();
1,694✔
44
        this.flagTopLevelStatements();
1,694✔
45
        //only validate the file if it was actually parsed (skip files containing typedefs)
46
        if (!this.event.file.hasTypedef) {
1,694✔
47
            this.validateTopOfFileStatements();
1,693✔
48
            this.validateTypecastStatements();
1,693✔
49
        }
50

51
        this.event.file.ast.bsConsts = bsConstsBackup;
1,694✔
52
        unlinkGlobalSymbolTable();
1,694✔
53
    }
54

55
    /**
56
     * Walk the full AST
57
     */
58
    private walk() {
59
        const isBrighterscript = this.event.file.parser.options.mode === ParseMode.BrighterScript;
1,694✔
60

61
        const visitor = createVisitor({
1,694✔
62
            MethodStatement: (node) => {
63
                //add the `super` symbol to class methods
64
                if (isClassStatement(node.parent) && node.parent.hasParentClass()) {
232✔
65
                    const data: ExtraSymbolData = {};
70✔
66
                    const parentClassType = node.parent.parentClassName.getType({ flags: SymbolTypeFlag.typetime, data: data });
70✔
67
                    node.func.body.getSymbolTable().addSymbol('super', { ...data, isInstance: true }, parentClassType, SymbolTypeFlag.runtime);
70✔
68
                }
69
            },
70
            CallfuncExpression: (node) => {
71
                if (node.args.length > 5) {
19✔
72
                    this.event.program.diagnostics.register({
1✔
73
                        ...DiagnosticMessages.callfuncHasToManyArgs(node.args.length),
74
                        location: node.tokens.methodName.location
75
                    });
76
                }
77
            },
78
            EnumStatement: (node) => {
79
                this.validateDeclarationLocations(node, 'enum', () => util.createBoundingRange(node.tokens.enum, node.tokens.name));
141✔
80

81
                this.validateEnumDeclaration(node);
141✔
82

83
                //register this enum declaration
84
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime });
141✔
85
                // eslint-disable-next-line no-bitwise
86
                node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, { definingNode: node }, nodeType, SymbolTypeFlag.typetime | SymbolTypeFlag.runtime);
141!
87
            },
88
            ClassStatement: (node) => {
89
                this.validateDeclarationLocations(node, 'class', () => util.createBoundingRange(node.tokens.class, node.tokens.name));
409✔
90

91
                //register this class
92
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime });
409✔
93
                node.getSymbolTable().addSymbol('m', { definingNode: node, isInstance: true }, nodeType, SymbolTypeFlag.runtime);
409✔
94
                // eslint-disable-next-line no-bitwise
95
                node.parent.getSymbolTable()?.addSymbol(node.tokens.name?.text, { definingNode: node }, nodeType, SymbolTypeFlag.typetime | SymbolTypeFlag.runtime);
409!
96

97
                if (node.findAncestor(isNamespaceStatement)) {
409✔
98
                    //add the transpiled name for namespaced constructors to the root symbol table
99
                    const transpiledClassConstructor = node.getName(ParseMode.BrightScript);
128✔
100

101
                    this.event.file.parser.ast.symbolTable.addSymbol(
128✔
102
                        transpiledClassConstructor,
103
                        { definingNode: node },
104
                        node.getConstructorType(),
105
                        // eslint-disable-next-line no-bitwise
106
                        SymbolTypeFlag.runtime | SymbolTypeFlag.postTranspile
107
                    );
108
                }
109
            },
110
            AssignmentStatement: (node) => {
111
                const data: ExtraSymbolData = {};
658✔
112
                //register this variable
113
                let nodeType = node.getType({ flags: SymbolTypeFlag.runtime, data: data });
658✔
114
                if (isInvalidType(nodeType) || isVoidType(nodeType)) {
658✔
115
                    nodeType = DynamicType.instance;
12✔
116
                }
117
                node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true, isFromDocComment: data.isFromDocComment }, nodeType, SymbolTypeFlag.runtime);
658!
118
            },
119
            DottedSetStatement: (node) => {
120
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
93✔
121
            },
122
            IndexedSetStatement: (node) => {
123
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
19✔
124
            },
125
            ForEachStatement: (node) => {
126
                //register the for loop variable
127
                const loopTargetType = node.target.getType({ flags: SymbolTypeFlag.runtime });
24✔
128
                let loopVarType = isArrayType(loopTargetType) ? loopTargetType.defaultType : DynamicType.instance;
24✔
129
                if (!loopTargetType.isResolvable()) {
24✔
130
                    loopVarType = new ArrayDefaultTypeReferenceType(loopTargetType);
1✔
131
                }
132
                node.parent.getSymbolTable()?.addSymbol(node.tokens.item.text, { definingNode: node, isInstance: true }, loopVarType, SymbolTypeFlag.runtime);
24!
133
            },
134
            NamespaceStatement: (node) => {
135
                this.validateDeclarationLocations(node, 'namespace', () => util.createBoundingRange(node.tokens.namespace, node.nameExpression));
588✔
136
                //Namespace Types are added at the Scope level - This is handled when the SymbolTables get linked
137
            },
138
            FunctionStatement: (node) => {
139
                this.validateDeclarationLocations(node, 'function', () => util.createBoundingRange(node.func.tokens.functionType, node.tokens.name));
1,701✔
140
                const funcType = node.getType({ flags: SymbolTypeFlag.typetime });
1,701✔
141

142
                if (node.tokens.name?.text) {
1,701!
143
                    node.parent.getSymbolTable().addSymbol(
1,701✔
144
                        node.tokens.name.text,
145
                        { definingNode: node },
146
                        funcType,
147
                        SymbolTypeFlag.runtime
148
                    );
149
                }
150

151
                const namespace = node.findAncestor(isNamespaceStatement);
1,701✔
152
                //this function is declared inside a namespace
153
                if (namespace) {
1,701✔
154
                    namespace.getSymbolTable().addSymbol(
396✔
155
                        node.tokens.name?.text,
1,188!
156
                        { definingNode: node },
157
                        funcType,
158
                        SymbolTypeFlag.runtime
159
                    );
160
                    //add the transpiled name for namespaced functions to the root symbol table
161
                    const transpiledNamespaceFunctionName = node.getName(ParseMode.BrightScript);
396✔
162

163
                    this.event.file.parser.ast.symbolTable.addSymbol(
396✔
164
                        transpiledNamespaceFunctionName,
165
                        { definingNode: node },
166
                        funcType,
167
                        // eslint-disable-next-line no-bitwise
168
                        SymbolTypeFlag.runtime | SymbolTypeFlag.postTranspile
169
                    );
170
                }
171
            },
172
            FunctionExpression: (node) => {
173
                const funcSymbolTable = node.getSymbolTable();
1,964✔
174
                const isInlineFunc = !(isFunctionStatement(node.parent) || isMethodStatement(node.parent));
1,964✔
175
                if (isInlineFunc) {
1,964✔
176
                    // symbol table should not include any symbols from parent func
177
                    funcSymbolTable.pushParentProvider(() => node.findAncestor<Body>(isBody).getSymbolTable());
117✔
178
                }
179
                if (!funcSymbolTable?.hasSymbol('m', SymbolTypeFlag.runtime) || isInlineFunc) {
1,964!
180
                    if (!isTypecastStatement(node.body?.statements?.[0])) {
31!
181
                        funcSymbolTable?.addSymbol('m', { isInstance: true }, new AssociativeArrayType(), SymbolTypeFlag.runtime);
30!
182
                    }
183
                }
184
                this.validateFunctionParameterCount(node);
1,964✔
185
            },
186
            FunctionParameterExpression: (node) => {
187
                const paramName = node.tokens.name?.text;
1,003!
188
                const data: ExtraSymbolData = {};
1,003✔
189
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime, data: data });
1,003✔
190
                // add param symbol at expression level, so it can be used as default value in other params
191
                const funcExpr = node.findAncestor<FunctionExpression>(isFunctionExpression);
1,003✔
192
                const funcSymbolTable = funcExpr?.getSymbolTable();
1,003!
193
                funcSymbolTable?.addSymbol(paramName, { definingNode: node, isInstance: true, isFromDocComment: data.isFromDocComment }, nodeType, SymbolTypeFlag.runtime);
1,003!
194

195
                //also add param symbol at block level, as it may be redefined, and if so, should show a union
196
                funcExpr.body.getSymbolTable()?.addSymbol(paramName, { definingNode: node, isInstance: true, isFromDocComment: data.isFromDocComment }, nodeType, SymbolTypeFlag.runtime);
1,003!
197
            },
198
            InterfaceStatement: (node) => {
199
                this.validateDeclarationLocations(node, 'interface', () => util.createBoundingRange(node.tokens.interface, node.tokens.name));
125✔
200

201
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime });
125✔
202
                // eslint-disable-next-line no-bitwise
203
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node }, nodeType, SymbolTypeFlag.typetime);
125✔
204
            },
205
            ConstStatement: (node) => {
206
                this.validateDeclarationLocations(node, 'const', () => util.createBoundingRange(node.tokens.const, node.tokens.name));
145✔
207
                const nodeType = node.getType({ flags: SymbolTypeFlag.runtime });
145✔
208
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true }, nodeType, SymbolTypeFlag.runtime);
145✔
209
            },
210
            CatchStatement: (node) => {
211
                //brs and bs both support variableExpression for the exception variable
212
                if (isVariableExpression(node.exceptionVariableExpression)) {
9✔
213
                    node.parent.getSymbolTable().addSymbol(
7✔
214
                        node.exceptionVariableExpression.getName(),
215
                        { definingNode: node, isInstance: true },
216
                        //TODO I think we can produce a slightly more specific type here (like an AA but with the known exception properties)
217
                        DynamicType.instance,
218
                        SymbolTypeFlag.runtime
219
                    );
220
                    //brighterscript allows catch without an exception variable
221
                } else if (isBrighterscript && !node.exceptionVariableExpression) {
2!
222
                    //this is fine
223

224
                    //brighterscript allows a typecast expression here
UNCOV
225
                } else if (isBrighterscript && isTypecastExpression(node.exceptionVariableExpression) && isVariableExpression(node.exceptionVariableExpression.obj)) {
×
UNCOV
226
                    node.parent.getSymbolTable().addSymbol(
×
227
                        node.exceptionVariableExpression.obj.getName(),
228
                        { definingNode: node, isInstance: true },
229
                        node.exceptionVariableExpression.getType({ flags: SymbolTypeFlag.runtime }),
230
                        SymbolTypeFlag.runtime
231
                    );
232

233
                    //no other expressions are allowed here
234
                } else {
UNCOV
235
                    this.event.program.diagnostics.register({
×
236
                        ...DiagnosticMessages.expectedExceptionVarToFollowCatch(),
237
                        location: node.exceptionVariableExpression?.location ?? node.tokens.catch?.location
×
238
                    });
239
                }
240
            },
241
            DimStatement: (node) => {
242
                if (node.tokens.name) {
18!
243
                    node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true }, node.getType({ flags: SymbolTypeFlag.runtime }), SymbolTypeFlag.runtime);
18✔
244
                }
245
            },
246
            ContinueStatement: (node) => {
247
                this.validateContinueStatement(node);
8✔
248
            },
249
            TypecastStatement: (node) => {
250
                node.parent.getSymbolTable().addSymbol('m', { definingNode: node, doNotMerge: true, isInstance: true }, node.getType({ flags: SymbolTypeFlag.typetime }), SymbolTypeFlag.runtime);
19✔
251
            },
252
            ConditionalCompileConstStatement: (node) => {
253
                const assign = node.assignment;
10✔
254
                const constNameLower = assign.tokens.name?.text.toLowerCase();
10!
255
                const astBsConsts = this.event.file.ast.bsConsts;
10✔
256
                if (isLiteralExpression(assign.value)) {
10!
257
                    astBsConsts.set(constNameLower, assign.value.tokens.value.text.toLowerCase() === 'true');
10✔
UNCOV
258
                } else if (isVariableExpression(assign.value)) {
×
UNCOV
259
                    if (this.validateConditionalCompileConst(assign.value.tokens.name)) {
×
260
                        astBsConsts.set(constNameLower, astBsConsts.get(assign.value.tokens.name.text.toLowerCase()));
×
261
                    }
262
                }
263
            },
264
            ConditionalCompileStatement: (node) => {
265
                this.validateConditionalCompileConst(node.tokens.condition);
22✔
266
            },
267
            ConditionalCompileErrorStatement: (node) => {
268
                this.event.program.diagnostics.register({
1✔
269
                    ...DiagnosticMessages.hashError(node.tokens.message.text),
270
                    location: node.location
271
                });
272
            },
273
            AliasStatement: (node) => {
274
                // eslint-disable-next-line no-bitwise
275
                const targetType = node.value.getType({ flags: SymbolTypeFlag.typetime | SymbolTypeFlag.runtime });
30✔
276

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

280
            },
281
            AstNode: (node) => {
282
                if (isStatement(node)) {
23,836✔
283
                    this.validateAnnotations(node);
9,609✔
284
                }
285
                this.handleDocTags(node);
23,836✔
286
            }
287
        });
288

289
        this.event.file.ast.walk((node, parent) => {
1,694✔
290
            visitor(node, parent);
23,836✔
291
        }, {
292
            walkMode: WalkMode.visitAllRecursive
293
        });
294
    }
295

296

297
    private handleDocTags(node: AstNode) {
298
        //check for doc comments
299
        if (!node.leadingTrivia || node.leadingTrivia.length === 0) {
23,836✔
300
            return;
4,190✔
301
        }
302
        const doc = brsDocParser.parseNode(node);
19,646✔
303
        if (doc.tags.length === 0) {
19,646✔
304
            return;
19,600✔
305
        }
306

307
        let funcExpr = node.findAncestor<FunctionExpression>(isFunctionExpression);
46✔
308
        if (funcExpr) {
46✔
309
            // handle comment tags inside a function expression
310
            this.processDocTagsInFunction(doc, node, funcExpr);
8✔
311
        } else {
312
            //handle comment tags outside of a function expression
313
            this.processDocTagsAtTopLevel(doc, node);
38✔
314
        }
315
    }
316

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

320
        // For example, declaring variable types:
321
        // const symbolTable = funcExpr.body.getSymbolTable();
322

323
        // for (const varTag of doc.getAllTags(BrsDocTagKind.Var)) {
324
        //     const varName = (varTag as BrsDocParamTag).name;
325
        //     const varTypeStr = (varTag as BrsDocParamTag).type;
326
        //     const data: ExtraSymbolData = {};
327
        //     const type = doc.getTypeFromContext(varTypeStr, node, { flags: SymbolTypeFlag.typetime, fullName: varTypeStr, data: data, tableProvider: () => symbolTable });
328
        //     if (type) {
329
        //         symbolTable.addSymbol(varName, { ...data, isFromDocComment: true }, type, SymbolTypeFlag.runtime);
330
        //     }
331
        // }
332
    }
333

334
    private processDocTagsAtTopLevel(doc: BrightScriptDoc, node: AstNode) {
335
        //TODO:
336
        // - handle import statements?
337
        // - handle library statements?
338
        // - handle typecast statements?
339
        // - handle alias statements?
340
        // - handle const statements?
341
        // - allow interface definitions?
342
    }
343

344
    /**
345
     * Validate that a statement is defined in one of these specific locations
346
     *  - the root of the AST
347
     *  - inside a namespace
348
     * This is applicable to things like FunctionStatement, ClassStatement, NamespaceStatement, EnumStatement, InterfaceStatement
349
     */
350
    private validateDeclarationLocations(statement: Statement, keyword: string, rangeFactory?: () => (Range | undefined)) {
351
        //if nested inside a namespace, or defined at the root of the AST (i.e. in a body that has no parent)
352
        const isOkDeclarationLocation = (parentNode) => {
3,109✔
353
            return isNamespaceStatement(parentNode?.parent) || (isBody(parentNode) && !parentNode?.parent);
3,114!
354
        };
355
        if (isOkDeclarationLocation(statement.parent)) {
3,109✔
356
            return;
3,091✔
357
        }
358

359
        // is this in a top levelconditional compile?
360
        if (isConditionalCompileStatement(statement.parent?.parent)) {
18!
361
            if (isOkDeclarationLocation(statement.parent.parent.parent)) {
5✔
362
                return;
4✔
363
            }
364
        }
365

366
        //the statement was defined in the wrong place. Flag it.
367
        this.event.program.diagnostics.register({
14✔
368
            ...DiagnosticMessages.keywordMustBeDeclaredAtNamespaceLevel(keyword),
369
            location: rangeFactory ? util.createLocationFromFileRange(this.event.file, rangeFactory()) : statement.location
14!
370
        });
371
    }
372

373
    private validateFunctionParameterCount(func: FunctionExpression) {
374
        if (func.parameters.length > CallExpression.MaximumArguments) {
1,964✔
375
            //flag every parameter over the limit
376
            for (let i = CallExpression.MaximumArguments; i < func.parameters.length; i++) {
2✔
377
                this.event.program.diagnostics.register({
3✔
378
                    ...DiagnosticMessages.tooManyCallableParameters(func.parameters.length, CallExpression.MaximumArguments),
379
                    location: func.parameters[i]?.tokens.name?.location ?? func.parameters[i]?.location ?? func.location
36!
380
                });
381
            }
382
        }
383
    }
384

385
    private validateEnumDeclaration(stmt: EnumStatement) {
386
        const members = stmt.getMembers();
141✔
387
        //the enum data type is based on the first member value
388
        const enumValueKind = (members.find(x => x.value)?.value as LiteralExpression)?.tokens?.value?.kind ?? TokenKind.IntegerLiteral;
198✔
389
        const memberNames = new Set<string>();
141✔
390
        for (const member of members) {
141✔
391
            const memberNameLower = member.name?.toLowerCase();
284!
392

393
            /**
394
             * flag duplicate member names
395
             */
396
            if (memberNames.has(memberNameLower)) {
284✔
397
                this.event.program.diagnostics.register({
1✔
398
                    ...DiagnosticMessages.duplicateIdentifier(member.name),
399
                    location: member.location
400
                });
401
            } else {
402
                memberNames.add(memberNameLower);
283✔
403
            }
404

405
            //Enforce all member values are the same type
406
            this.validateEnumValueTypes(member, enumValueKind);
284✔
407
        }
408
    }
409

410
    private validateEnumValueTypes(member: EnumMemberStatement, enumValueKind: TokenKind) {
411
        let memberValueKind: TokenKind;
412
        let memberValue: Expression;
413
        if (isUnaryExpression(member.value)) {
284✔
414
            memberValueKind = (member.value?.right as LiteralExpression)?.tokens?.value?.kind;
2!
415
            memberValue = member.value?.right;
2!
416
        } else {
417
            memberValueKind = (member.value as LiteralExpression)?.tokens?.value?.kind;
282✔
418
            memberValue = member.value;
282✔
419
        }
420
        const range = (memberValue ?? member)?.location?.range;
284!
421
        if (
284✔
422
            //is integer enum, has value, that value type is not integer
423
            (enumValueKind === TokenKind.IntegerLiteral && memberValueKind && memberValueKind !== enumValueKind) ||
961✔
424
            //has value, that value is not a literal
425
            (memberValue && !isLiteralExpression(memberValue))
426
        ) {
427
            this.event.program.diagnostics.register({
6✔
428
                ...DiagnosticMessages.enumValueMustBeType(
429
                    enumValueKind.replace(/literal$/i, '').toLowerCase()
430
                ),
431
                location: util.createLocationFromFileRange(this.event.file, range)
432
            });
433
        }
434

435
        //is non integer value
436
        if (enumValueKind !== TokenKind.IntegerLiteral) {
284✔
437
            //default value present
438
            if (memberValueKind) {
101✔
439
                //member value is same as enum
440
                if (memberValueKind !== enumValueKind) {
99✔
441
                    this.event.program.diagnostics.register({
1✔
442
                        ...DiagnosticMessages.enumValueMustBeType(
443
                            enumValueKind.replace(/literal$/i, '').toLowerCase()
444
                        ),
445
                        location: util.createLocationFromFileRange(this.event.file, range)
446
                    });
447
                }
448

449
                //default value missing
450
            } else {
451
                this.event.program.diagnostics.register({
2✔
452
                    ...DiagnosticMessages.enumValueIsRequired(
453
                        enumValueKind.replace(/literal$/i, '').toLowerCase()
454
                    ),
455
                    location: util.createLocationFromFileRange(this.event.file, range)
456
                });
457
            }
458
        }
459
    }
460

461

462
    private validateConditionalCompileConst(ccConst: Token) {
463
        const isBool = ccConst.kind === TokenKind.True || ccConst.kind === TokenKind.False;
22✔
464
        if (!isBool && !this.event.file.ast.bsConsts.has(ccConst.text.toLowerCase())) {
22✔
465
            this.event.program.diagnostics.register({
2✔
466
                ...DiagnosticMessages.hashConstDoesNotExist(),
467
                location: ccConst.location
468
            });
469
            return false;
2✔
470
        }
471
        return true;
20✔
472
    }
473

474
    /**
475
     * Find statements defined at the top level (or inside a namespace body) that are not allowed to be there
476
     */
477
    private flagTopLevelStatements() {
478
        const statements = [...this.event.file.ast.statements];
1,694✔
479
        while (statements.length > 0) {
1,694✔
480
            const statement = statements.pop();
3,338✔
481
            if (isNamespaceStatement(statement)) {
3,338✔
482
                statements.push(...statement.body.statements);
583✔
483
            } else {
484
                //only allow these statement types
485
                if (
2,755✔
486
                    !isFunctionStatement(statement) &&
6,173✔
487
                    !isClassStatement(statement) &&
488
                    !isEnumStatement(statement) &&
489
                    !isInterfaceStatement(statement) &&
490
                    !isLibraryStatement(statement) &&
491
                    !isImportStatement(statement) &&
492
                    !isConstStatement(statement) &&
493
                    !isTypecastStatement(statement) &&
494
                    !isConditionalCompileConstStatement(statement) &&
495
                    !isConditionalCompileErrorStatement(statement) &&
496
                    !isConditionalCompileStatement(statement) &&
497
                    !isAliasStatement(statement)
498
                ) {
499
                    this.event.program.diagnostics.register({
8✔
500
                        ...DiagnosticMessages.unexpectedStatementOutsideFunction(),
501
                        location: statement.location
502
                    });
503
                }
504
            }
505
        }
506
    }
507

508
    private getTopOfFileStatements() {
509
        let topOfFileIncludeStatements = [] as Array<LibraryStatement | ImportStatement | TypecastStatement | AliasStatement>;
3,386✔
510
        for (let stmt of this.event.file.parser.ast.statements) {
3,386✔
511
            //if we found a non-library statement, this statement is not at the top of the file
512
            if (isLibraryStatement(stmt) || isImportStatement(stmt) || isTypecastStatement(stmt) || isAliasStatement(stmt)) {
3,708✔
513
                topOfFileIncludeStatements.push(stmt);
452✔
514
            } else {
515
                //break out of the loop, we found all of our library statements
516
                break;
3,256✔
517
            }
518
        }
519
        return topOfFileIncludeStatements;
3,386✔
520
    }
521

522
    private validateTopOfFileStatements() {
523
        let topOfFileStatements = this.getTopOfFileStatements();
1,693✔
524

525
        let statements = [
1,693✔
526
            // eslint-disable-next-line @typescript-eslint/dot-notation
527
            ...this.event.file['_cachedLookups'].libraryStatements,
528
            // eslint-disable-next-line @typescript-eslint/dot-notation
529
            ...this.event.file['_cachedLookups'].importStatements,
530
            // eslint-disable-next-line @typescript-eslint/dot-notation
531
            ...this.event.file['_cachedLookups'].aliasStatements
532
        ];
533
        for (let result of statements) {
1,693✔
534
            //if this statement is not one of the top-of-file statements,
535
            //then add a diagnostic explaining that it is invalid
536
            if (!topOfFileStatements.includes(result)) {
224✔
537
                if (isLibraryStatement(result)) {
5✔
538
                    this.event.program.diagnostics.register({
2✔
539
                        ...DiagnosticMessages.unexpectedStatementLocation('library', 'at the top of the file'),
540
                        location: result.location
541
                    });
542
                } else if (isImportStatement(result)) {
3✔
543
                    this.event.program.diagnostics.register({
1✔
544
                        ...DiagnosticMessages.unexpectedStatementLocation('import', 'at the top of the file'),
545
                        location: result.location
546
                    });
547
                } else if (isAliasStatement(result)) {
2!
548
                    this.event.program.diagnostics.register({
2✔
549
                        ...DiagnosticMessages.unexpectedStatementLocation('alias', 'at the top of the file'),
550
                        location: result.location
551
                    });
552
                }
553
            }
554
        }
555
    }
556

557
    private validateTypecastStatements() {
558
        let topOfFileTypecastStatements = this.getTopOfFileStatements().filter(stmt => isTypecastStatement(stmt));
1,693✔
559

560
        //check only one `typecast` statement at "top" of file (eg. before non import/library statements)
561
        for (let i = 1; i < topOfFileTypecastStatements.length; i++) {
1,693✔
562
            const typecastStmt = topOfFileTypecastStatements[i];
1✔
563
            this.event.program.diagnostics.register({
1✔
564
                ...DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of function or namespace'),
565
                location: typecastStmt.location
566
            });
567
        }
568

569
        // eslint-disable-next-line @typescript-eslint/dot-notation
570
        for (let result of this.event.file['_cachedLookups'].typecastStatements) {
1,693✔
571
            let isBadTypecastObj = false;
19✔
572
            if (!isVariableExpression(result.typecastExpression.obj)) {
19✔
573
                isBadTypecastObj = true;
1✔
574
            } else if (result.typecastExpression.obj.tokens.name.text.toLowerCase() !== 'm') {
18✔
575
                isBadTypecastObj = true;
1✔
576
            }
577
            if (isBadTypecastObj) {
19✔
578
                this.event.program.diagnostics.register({
2✔
579
                    ...DiagnosticMessages.invalidTypecastStatementApplication(util.getAllDottedGetPartsAsString(result.typecastExpression.obj)),
580
                    location: result.typecastExpression.obj.location
581
                });
582
            }
583

584
            if (topOfFileTypecastStatements.includes(result)) {
19✔
585
                // already validated
586
                continue;
7✔
587
            }
588

589
            const block = result.findAncestor<Body | Block>(node => (isBody(node) || isBlock(node)));
12✔
590
            const isFirst = block?.statements[0] === result;
12!
591
            const isAllowedBlock = (isBody(block) || isFunctionExpression(block.parent) || isNamespaceStatement(block.parent));
12!
592

593
            if (!isFirst || !isAllowedBlock) {
12✔
594
                this.event.program.diagnostics.register({
3✔
595
                    ...DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of function or namespace'),
596
                    location: result.location
597
                });
598
            }
599
        }
600
    }
601

602
    private validateContinueStatement(statement: ContinueStatement) {
603
        const validateLoopTypeMatch = (expectedLoopType: TokenKind) => {
8✔
604
            //coerce ForEach to For
605
            expectedLoopType = expectedLoopType === TokenKind.ForEach ? TokenKind.For : expectedLoopType;
7✔
606
            const actualLoopType = statement.tokens.loopType;
7✔
607
            if (actualLoopType && expectedLoopType?.toLowerCase() !== actualLoopType.text?.toLowerCase()) {
7!
608
                this.event.program.diagnostics.register({
3✔
609
                    location: statement.tokens.loopType.location,
610
                    ...DiagnosticMessages.expectedToken(expectedLoopType)
611
                });
612
            }
613
        };
614

615
        //find the parent loop statement
616
        const parent = statement.findAncestor<WhileStatement | ForStatement | ForEachStatement>((node) => {
8✔
617
            if (isWhileStatement(node)) {
18✔
618
                validateLoopTypeMatch(node.tokens.while.kind);
3✔
619
                return true;
3✔
620
            } else if (isForStatement(node)) {
15✔
621
                validateLoopTypeMatch(node.tokens.for.kind);
3✔
622
                return true;
3✔
623
            } else if (isForEachStatement(node)) {
12✔
624
                validateLoopTypeMatch(node.tokens.forEach.kind);
1✔
625
                return true;
1✔
626
            }
627
        });
628
        //flag continue statements found outside of a loop
629
        if (!parent) {
8✔
630
            this.event.program.diagnostics.register({
1✔
631
                location: statement.location,
632
                ...DiagnosticMessages.illegalContinueStatement()
633
            });
634
        }
635
    }
636

637
    /**
638
     * Validate that there are no optional chaining operators on the left-hand-side of an assignment, indexed set, or dotted get
639
     */
640
    private validateNoOptionalChainingInVarSet(parent: AstNode, children: AstNode[]) {
641
        const nodes = [...children, parent];
112✔
642
        //flag optional chaining anywhere in the left of this statement
643
        while (nodes.length > 0) {
112✔
644
            const node = nodes.shift();
224✔
645
            if (
224✔
646
                // a?.b = true or a.b?.c = true
647
                ((isDottedSetStatement(node) || isDottedGetExpression(node)) && node.tokens.dot?.kind === TokenKind.QuestionDot) ||
1,264!
648
                // a.b?[2] = true
649
                (isIndexedGetExpression(node) && (node?.tokens.questionDot?.kind === TokenKind.QuestionDot || node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare)) ||
36!
650
                // a?[1] = true
651
                (isIndexedSetStatement(node) && node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare)
57!
652
            ) {
653
                //try to highlight the entire left-hand-side expression if possible
654
                let range: Range;
655
                if (isDottedSetStatement(parent)) {
8✔
656
                    range = util.createBoundingRange(parent.obj?.location, parent.tokens.dot, parent.tokens.name);
5!
657
                } else if (isIndexedSetStatement(parent)) {
3!
658
                    range = util.createBoundingRange(parent.obj?.location, parent.tokens.openingSquare, ...parent.indexes, parent.tokens.closingSquare);
3!
659
                } else {
UNCOV
660
                    range = node.location?.range;
×
661
                }
662

663
                this.event.program.diagnostics.register({
8✔
664
                    ...DiagnosticMessages.noOptionalChainingInLeftHandSideOfAssignment(),
665
                    location: util.createLocationFromFileRange(this.event.file, range)
666
                });
667
            }
668

669
            if (node === parent) {
224✔
670
                break;
112✔
671
            } else {
672
                nodes.push(node.parent);
112✔
673
            }
674
        }
675
    }
676

677
    private validateAnnotations(statement: Statement) {
678
        if (!statement.annotations || statement.annotations.length < 1) {
9,609✔
679
            return;
9,581✔
680
        }
681

682
        const symbolTable = this.event.program.pluginAnnotationTable;
28✔
683
        const extraData: ExtraSymbolData = {};
28✔
684

685
        for (const annotation of statement.annotations) {
28✔
686
            const annotationType = symbolTable.getSymbolType(annotation.name, { flags: SymbolTypeFlag.annotation, data: extraData });
37✔
687

688
            if (!annotationType || !annotationType?.isResolvable()) {
37!
689
                this.event.program.diagnostics.register({
1✔
690
                    ...DiagnosticMessages.cannotFindAnnotation(annotation.name),
691
                    location: brsDocParser.getTypeLocationFromToken(annotation.tokens.name) ?? annotation.location
3!
692
                });
693
                continue;
1✔
694
            }
695
            if (!isTypedFunctionType(annotationType)) {
36✔
696
                // TODO: handle multiple function definitions - in that case this would be a UnionType
697
                continue;
3✔
698
            }
699
            const { minParams, maxParams } = annotationType.getMinMaxParamCount();
33✔
700
            let expCallArgCount = annotation.call?.args.length ?? 0;
33✔
701
            if (expCallArgCount > maxParams || expCallArgCount < minParams) {
33✔
702
                let minMaxParamsText = minParams === maxParams ? maxParams : `${minParams}-${maxParams}`;
1!
703
                this.event.program.diagnostics.register({
1✔
704
                    ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount),
705
                    location: annotation.location
706
                });
707
            }
708

709
            // validate the arg types - very similar to code in ScopeValidator
710
            let paramIndex = 0;
33✔
711
            for (let arg of annotation.call?.args ?? []) {
33✔
712
                const data = {} as ExtraSymbolData;
16✔
713
                let argType = arg.getType({ flags: SymbolTypeFlag.runtime, data: data, onlyAllowLiterals: true });
16✔
714

715
                if (!argType || !argType.isResolvable()) {
16✔
716
                    this.event.program.diagnostics.register({
2✔
717
                        ...DiagnosticMessages.expectedLiteralValue('in annotation argument', util.getAllDottedGetPartsAsString(arg)),
718
                        location: arg.location
719
                    });
720
                    break;
2✔
721
                }
722
                let paramType = annotationType.params[paramIndex]?.type;
14✔
723
                if (!paramType) {
14✔
724
                    // unable to find a paramType -- maybe there are more args than params
725
                    break;
1✔
726
                }
727

728
                if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
13!
NEW
729
                    argType = data.definingNode?.getConstructorType();
×
730
                }
731

732
                const compatibilityData: TypeCompatibilityData = {};
13✔
733
                if (!argType || !argType.isResolvable() || !paramType?.isTypeCompatible(argType, compatibilityData)) {
13!
734

735
                    const argTypeStr = argType?.toString() ?? UninitializedType.instance.toString();
2!
736

737
                    this.event.program.diagnostics.register({
2✔
738
                        ...DiagnosticMessages.argumentTypeMismatch(argTypeStr, paramType.toString(), compatibilityData),
739
                        location: arg.location
740
                    });
741
                }
742
                paramIndex++;
13✔
743
            }
744
        }
745
    }
746

747
}
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