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

rokucommunity / brighterscript / #15219

22 Feb 2026 02:27AM UTC coverage: 87.193% (-0.006%) from 87.199%
#15219

push

web-flow
Merge d0c9a16a7 into 1556715dd

14749 of 17875 branches covered (82.51%)

Branch coverage included in aggregate %.

107 of 117 new or added lines in 19 files covered. (91.45%)

161 existing lines in 16 files now uncovered.

15493 of 16809 relevant lines covered (92.17%)

25604.58 hits per line

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

86.31
/src/bscPlugin/validation/BrsFileValidator.ts
1
import { isAliasStatement, isBlock, isBody, 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 } 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

21
export class BrsFileValidator {
1✔
22
    constructor(
23
        public event: ValidateFileEvent<BrsFile>
2,140✔
24
    ) {
25
    }
26

27

28
    public process() {
29
        const unlinkGlobalSymbolTable = this.event.file.parser.symbolTable.pushParentProvider(() => this.event.program.globalScope.symbolTable);
10,055✔
30

31
        util.validateTooDeepFile(this.event.file);
2,140✔
32

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

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

41
        this.walk();
2,140✔
42
        this.flagTopLevelStatements();
2,140✔
43
        //only validate the file if it was actually parsed (skip files containing typedefs)
44
        if (!this.event.file.hasTypedef) {
2,140✔
45
            this.validateTopOfFileStatements();
2,139✔
46
            this.validateTypecastStatements();
2,139✔
47
        }
48

49
        this.event.file.ast.bsConsts = bsConstsBackup;
2,140✔
50
        unlinkGlobalSymbolTable();
2,140✔
51
    }
52

53
    /**
54
     * Walk the full AST
55
     */
56
    private walk() {
57
        const isBrighterscript = this.event.file.parser.options.mode === ParseMode.BrighterScript;
2,140✔
58

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

79
                this.validateEnumDeclaration(node);
155✔
80

81
                if (!node.tokens.name) {
155!
82
                    return;
×
83
                }
84
                //register this enum declaration
85
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime });
155✔
86
                // eslint-disable-next-line no-bitwise
87
                node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, { definingNode: node }, nodeType, SymbolTypeFlag.typetime | SymbolTypeFlag.runtime);
155!
88
            },
89
            ClassStatement: (node) => {
90
                if (!node?.tokens?.name) {
434!
91
                    return;
1✔
92
                }
93
                this.validateDeclarationLocations(node, 'class', () => util.createBoundingRange(node.tokens.class, node.tokens.name));
433✔
94

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

101
                if (node.findAncestor(isNamespaceStatement)) {
433✔
102
                    //add the transpiled name for namespaced constructors to the root symbol table
103
                    const transpiledClassConstructor = node.getName(ParseMode.BrightScript);
135✔
104

105
                    this.event.file.parser.ast.symbolTable.addSymbol(
135✔
106
                        transpiledClassConstructor,
107
                        { definingNode: node },
108
                        node.getConstructorType(),
109
                        // eslint-disable-next-line no-bitwise
110
                        SymbolTypeFlag.runtime | SymbolTypeFlag.postTranspile
111
                    );
112
                }
113
            },
114
            AssignmentStatement: (node) => {
115
                if (!node?.tokens?.name) {
870!
116
                    return;
×
117
                }
118
                if (isForStatement(node.parent) && node.parent.counterDeclaration === node) {
870✔
119
                    // for loop variable variable is added to the block symbol table elsewhere
120
                    return;
21✔
121
                }
122
                const data: ExtraSymbolData = {};
849✔
123
                //register this variable
124
                let nodeType = node.getType({ flags: SymbolTypeFlag.runtime, data: data });
849✔
125
                if (isInvalidType(nodeType) || isVoidType(nodeType)) {
849✔
126
                    nodeType = DynamicType.instance;
11✔
127
                }
128
                node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true, isFromDocComment: data.isFromDocComment, isFromCallFunc: data.isFromCallFunc }, nodeType, SymbolTypeFlag.runtime);
849!
129
            },
130
            DottedSetStatement: (node) => {
131
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
103✔
132
            },
133
            IndexedSetStatement: (node) => {
134
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
19✔
135
            },
136
            ForEachStatement: (node) => {
137
                //registering the for loop variable happens in the visitor for Block, since the loop variable is scoped to the loop body
138
            },
139
            NamespaceStatement: (node) => {
140
                if (!node?.nameExpression) {
626!
141
                    return;
×
142
                }
143
                this.validateDeclarationLocations(node, 'namespace', () => util.createBoundingRange(node.tokens.namespace, node.nameExpression));
626✔
144
                //Namespace Types are added at the Scope level - This is handled when the SymbolTables get linked
145
            },
146
            FunctionStatement: (node) => {
147
                this.validateDeclarationLocations(node, 'function', () => util.createBoundingRange(node.func.tokens.functionType, node.tokens.name));
2,154✔
148
                const funcType = node.getType({ flags: SymbolTypeFlag.typetime });
2,154✔
149

150
                if (node.tokens.name?.text) {
2,154!
151
                    node.parent.getSymbolTable().addSymbol(
2,154✔
152
                        node.tokens.name.text,
153
                        { definingNode: node },
154
                        funcType,
155
                        SymbolTypeFlag.runtime
156
                    );
157
                }
158

159
                const namespace = node.findAncestor(isNamespaceStatement);
2,154✔
160
                //this function is declared inside a namespace
161
                if (namespace) {
2,154✔
162
                    namespace.getSymbolTable().addSymbol(
411✔
163
                        node.tokens.name?.text,
1,233!
164
                        { definingNode: node },
165
                        funcType,
166
                        SymbolTypeFlag.runtime
167
                    );
168
                    if (!node.tokens?.name) {
411!
169
                        return;
×
170
                    }
171
                    //add the transpiled name for namespaced functions to the root symbol table
172
                    const transpiledNamespaceFunctionName = node.getName(ParseMode.BrightScript);
411✔
173

174
                    this.event.file.parser.ast.symbolTable.addSymbol(
411✔
175
                        transpiledNamespaceFunctionName,
176
                        { definingNode: node },
177
                        funcType,
178
                        // eslint-disable-next-line no-bitwise
179
                        SymbolTypeFlag.runtime | SymbolTypeFlag.postTranspile
180
                    );
181
                }
182
            },
183
            FunctionExpression: (node) => {
184
                const funcSymbolTable = node.getSymbolTable();
2,450✔
185
                const isInlineFunc = !(isFunctionStatement(node.parent) || isMethodStatement(node.parent));
2,450✔
186
                if (isInlineFunc) {
2,450✔
187
                    // symbol table should not include any symbols from parent func
188
                    funcSymbolTable.pushParentProvider(() => node.findAncestor<Body>(isBody).getSymbolTable());
219✔
189
                }
190
                if (!funcSymbolTable?.hasSymbol('m', SymbolTypeFlag.runtime) || isInlineFunc) {
2,450!
191
                    if (!isTypecastStatement(node.body?.statements?.[0])) {
53!
192
                        // 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
193
                        funcSymbolTable?.addSymbol('m', { isInstance: true }, new AssociativeArrayType(), SymbolTypeFlag.runtime);
52!
194
                    }
195
                }
196
                this.validateFunctionParameterCount(node);
2,450✔
197
            },
198
            FunctionParameterExpression: (node) => {
199
                if (isTypedFunctionTypeExpression(node.parent)) {
1,329✔
200
                    return;
33✔
201
                }
202
                const paramName = node.tokens?.name?.text;
1,296!
203
                if (!paramName) {
1,296!
UNCOV
204
                    return;
×
205
                }
206
                const data: ExtraSymbolData = {};
1,296✔
207
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime, data: data });
1,296✔
208
                // add param symbol at expression level, so it can be used as default value in other params
209
                const funcExpr = node.findAncestor<FunctionExpression>(isFunctionExpression);
1,296✔
210
                const funcSymbolTable = funcExpr?.getSymbolTable();
1,296!
211
                const extraSymbolData: ExtraSymbolData = {
1,296✔
212
                    definingNode: node,
213
                    isInstance: true,
214
                    isFromDocComment: data.isFromDocComment,
215
                    description: data.description
216
                };
217
                funcSymbolTable?.addSymbol(paramName, extraSymbolData, nodeType, SymbolTypeFlag.runtime);
1,296!
218

219
                //also add param symbol at block level, as it may be redefined, and if so, should show a union
220
                funcExpr.body.getSymbolTable()?.addSymbol(paramName, extraSymbolData, nodeType, SymbolTypeFlag.runtime);
1,296!
221
            },
222
            InterfaceStatement: (node) => {
223
                if (!node.tokens.name) {
192!
UNCOV
224
                    return;
×
225
                }
226
                this.validateDeclarationLocations(node, 'interface', () => util.createBoundingRange(node.tokens.interface, node.tokens.name));
192✔
227

228
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime });
192✔
229
                // eslint-disable-next-line no-bitwise
230
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node }, nodeType, SymbolTypeFlag.typetime);
192✔
231
            },
232
            ConstStatement: (node) => {
233
                if (!node.tokens.name) {
178!
UNCOV
234
                    return;
×
235
                }
236
                this.validateDeclarationLocations(node, 'const', () => util.createBoundingRange(node.tokens.const, node.tokens.name));
178✔
237
                const nodeType = node.getType({ flags: SymbolTypeFlag.runtime });
178✔
238
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true }, nodeType, SymbolTypeFlag.runtime);
178✔
239
            },
240
            CatchStatement: (node) => {
241
                //brs and bs both support variableExpression for the exception variable
242
                if (isVariableExpression(node.exceptionVariableExpression)) {
15✔
243
                    node.parent.getSymbolTable().addSymbol(
13✔
244
                        node.exceptionVariableExpression.getName(),
245
                        { definingNode: node, isInstance: true },
246
                        //TODO I think we can produce a slightly more specific type here (like an AA but with the known exception properties)
247
                        DynamicType.instance,
248
                        SymbolTypeFlag.runtime
249
                    );
250
                    //brighterscript allows catch without an exception variable
251
                } else if (isBrighterscript && !node.exceptionVariableExpression) {
2!
252
                    //this is fine
253

254
                    //brighterscript allows a typecast expression here
UNCOV
255
                } else if (isBrighterscript && isTypecastExpression(node.exceptionVariableExpression) && isVariableExpression(node.exceptionVariableExpression.obj)) {
×
UNCOV
256
                    node.parent.getSymbolTable().addSymbol(
×
257
                        node.exceptionVariableExpression.obj.getName(),
258
                        { definingNode: node, isInstance: true },
259
                        node.exceptionVariableExpression.getType({ flags: SymbolTypeFlag.runtime }),
260
                        SymbolTypeFlag.runtime
261
                    );
262

263
                    //no other expressions are allowed here
264
                } else {
UNCOV
265
                    this.event.program.diagnostics.register({
×
266
                        ...DiagnosticMessages.expectedExceptionVarToFollowCatch(),
267
                        location: node.exceptionVariableExpression?.location ?? node.tokens.catch?.location
×
268
                    });
269
                }
270
            },
271
            DimStatement: (node) => {
272
                if (node.tokens.name) {
18!
273
                    node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true }, node.getType({ flags: SymbolTypeFlag.runtime }), SymbolTypeFlag.runtime);
18✔
274
                }
275
            },
276
            ReturnStatement: (node) => {
277
                const func = node.findAncestor<FunctionExpression>(isFunctionExpression);
532✔
278
                //these situations cannot have a value next to `return`
279
                if (
532✔
280
                    //`function as void`, `sub as void`
281
                    (isVariableExpression(func?.returnTypeExpression?.expression) && func.returnTypeExpression.expression.tokens.name.text?.toLowerCase() === 'void') ||
5,672!
282
                    //`sub` <without return value>
283
                    (func.tokens.functionType?.kind === TokenKind.Sub && !func.returnTypeExpression)
1,557!
284
                ) {
285
                    //there may not be a return value
286
                    if (node.value) {
19✔
287
                        this.event.program.diagnostics.register({
11✔
288
                            ...DiagnosticMessages.voidFunctionMayNotReturnValue(func.tokens.functionType?.text),
33!
289
                            location: node.location
290
                        });
291
                    }
292

293
                } else {
294
                    //there MUST be a return value
295
                    if (!node.value) {
513✔
296
                        this.event.program.diagnostics.register({
11✔
297
                            ...DiagnosticMessages.nonVoidFunctionMustReturnValue(func?.tokens.functionType?.text),
66!
298
                            location: node.location
299
                        });
300
                    }
301
                }
302
            },
303
            ContinueStatement: (node) => {
304
                this.validateContinueStatement(node);
8✔
305
            },
306
            TypecastStatement: (node) => {
307
                const obj = node.typecastExpression.obj;
28✔
308
                if (isVariableExpression(obj)) {
28✔
309
                    node.parent.getSymbolTable().addSymbol(obj.tokens.name.text, { definingNode: node, doNotMerge: true, isInstance: true }, node.getType({ flags: SymbolTypeFlag.typetime }), SymbolTypeFlag.runtime);
26✔
310
                }
311
            },
312
            ConditionalCompileConstStatement: (node) => {
313
                const assign = node.assignment;
10✔
314
                const constNameLower = assign.tokens.name?.text.toLowerCase();
10!
315
                const astBsConsts = this.event.file.ast.bsConsts;
10✔
316
                if (isLiteralExpression(assign.value)) {
10!
317
                    astBsConsts.set(constNameLower, assign.value.tokens.value.text.toLowerCase() === 'true');
10✔
UNCOV
318
                } else if (isVariableExpression(assign.value)) {
×
UNCOV
319
                    if (this.validateConditionalCompileConst(assign.value.tokens.name)) {
×
UNCOV
320
                        astBsConsts.set(constNameLower, astBsConsts.get(assign.value.tokens.name.text.toLowerCase()));
×
321
                    }
322
                }
323
            },
324
            ConditionalCompileStatement: (node) => {
325
                this.validateConditionalCompileConst(node.tokens.condition);
24✔
326
            },
327
            ConditionalCompileErrorStatement: (node) => {
328
                this.event.program.diagnostics.register({
1✔
329
                    ...DiagnosticMessages.hashError(node.tokens.message.text),
330
                    location: node.location
331
                });
332
            },
333
            AliasStatement: (node) => {
334
                // eslint-disable-next-line no-bitwise
335
                const targetType = node.value.getType({ flags: SymbolTypeFlag.typetime | SymbolTypeFlag.runtime });
30✔
336

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

340
            },
341
            TypeStatement: (node) => {
342
                this.validateDeclarationLocations(node, 'type', () => util.createBoundingRange(node.tokens.type, node.tokens.name));
38✔
343
                const wrappedNodeType = node.getType({ flags: SymbolTypeFlag.runtime });
38✔
344
                const typeStmtType = new TypeStatementType(node.tokens.name.text, wrappedNodeType);
38✔
345
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, isFromTypeStatement: true }, typeStmtType, SymbolTypeFlag.typetime);
38✔
346

347
            },
348
            IfStatement: (node) => {
349
                this.setUpComplementSymbolTables(node, isIfStatement);
156✔
350
            },
351
            Block: (node) => {
352
                const blockSymbolTable = node.symbolTable;
2,773✔
353
                if (node.findAncestor<Block>(isFunctionExpression)) {
2,773✔
354
                    // this block is in a function. order matters!
355
                    blockSymbolTable.isOrdered = true;
2,769✔
356
                }
357
                if (!isFunctionExpression(node.parent) && node.parent) {
2,773✔
358
                    node.symbolTable.name = `Block-${node.parent.kind}@${node.location?.range?.start?.line}`;
323✔
359
                    // we're a block inside another block (or body). This block is a pocket in the bigger block
360
                    node.parent.getSymbolTable().addPocketTable({
323✔
361
                        index: node.parent.statementIndex,
362
                        table: blockSymbolTable,
363
                        // code always flows through ConditionalCompiles, because we walk according to defined BSConsts
364
                        willAlwaysBeExecuted: isConditionalCompileStatement(node.parent)
365
                    });
366

367
                    if (isForStatement(node.parent)) {
323✔
368
                        const counterDecl = node.parent.counterDeclaration;
21✔
369
                        const loopVar = counterDecl.tokens.name;
21✔
370
                        const loopVarType = counterDecl.getType({ flags: SymbolTypeFlag.runtime });
21✔
371
                        blockSymbolTable.addSymbol(loopVar.text, { isInstance: true }, loopVarType, SymbolTypeFlag.runtime);
21✔
372

373
                    } else if (isForEachStatement(node.parent)) {
302✔
374
                        const loopVarType = node.parent.getLoopVariableType({ flags: SymbolTypeFlag.runtime });
54✔
375
                        blockSymbolTable.addSymbol(node.parent.tokens.item.text, { isInstance: true }, loopVarType, SymbolTypeFlag.runtime);
54✔
376
                    }
377
                }
378
            },
379
            AstNode: (node) => {
380
                //check for doc comments
381
                if (!node.leadingTrivia || node.leadingTrivia.length === 0) {
30,620✔
382
                    return;
5,587✔
383
                }
384
                const doc = brsDocParser.parseNode(node);
25,033✔
385
                if (doc.tags.length === 0) {
25,033✔
386
                    return;
24,981✔
387
                }
388

389
                let funcExpr = node.findAncestor<FunctionExpression>(isFunctionExpression);
52✔
390
                if (funcExpr) {
52✔
391
                    // handle comment tags inside a function expression
392
                    this.processDocTagsInFunction(doc, node, funcExpr);
8✔
393
                } else {
394
                    //handle comment tags outside of a function expression
395
                    this.processDocTagsAtTopLevel(doc, node);
44✔
396
                }
397
            }
398
        });
399

400
        this.event.file.ast.walk((node, parent) => {
2,140✔
401
            visitor(node, parent);
30,620✔
402
        }, {
403
            walkMode: WalkMode.visitAllRecursive
404
        });
405
    }
406

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

410
        // For example, declaring variable types:
411
        // const symbolTable = funcExpr.body.getSymbolTable();
412

413
        // for (const varTag of doc.getAllTags(BrsDocTagKind.Var)) {
414
        //     const varName = (varTag as BrsDocParamTag).name;
415
        //     const varTypeStr = (varTag as BrsDocParamTag).type;
416
        //     const data: ExtraSymbolData = {};
417
        //     const type = doc.getTypeFromContext(varTypeStr, node, { flags: SymbolTypeFlag.typetime, fullName: varTypeStr, data: data, tableProvider: () => symbolTable });
418
        //     if (type) {
419
        //         symbolTable.addSymbol(varName, { ...data, isFromDocComment: true }, type, SymbolTypeFlag.runtime);
420
        //     }
421
        // }
422
    }
423

424
    private processDocTagsAtTopLevel(doc: BrightScriptDoc, node: AstNode) {
425
        //TODO:
426
        // - handle import statements?
427
        // - handle library statements?
428
        // - handle typecast statements?
429
        // - handle alias statements?
430
        // - handle const statements?
431
        // - allow interface definitions?
432
    }
433

434
    /**
435
     * Validate that a statement is defined in one of these specific locations
436
     *  - the root of the AST
437
     *  - inside a namespace
438
     * This is applicable to things like FunctionStatement, ClassStatement, NamespaceStatement, EnumStatement, InterfaceStatement
439
     */
440
    private validateDeclarationLocations(statement: Statement, keyword: string, rangeFactory?: () => (Range | undefined)) {
441
        //if nested inside a namespace, or defined at the root of the AST (i.e. in a body that has no parent)
442
        const isOkDeclarationLocation = (parentNode) => {
3,776✔
443
            return isNamespaceStatement(parentNode?.parent) || (isBody(parentNode) && !parentNode?.parent);
3,781!
444
        };
445
        if (isOkDeclarationLocation(statement.parent)) {
3,776✔
446
            return;
3,757✔
447
        }
448

449
        // is this in a top levelconditional compile?
450
        if (isConditionalCompileStatement(statement.parent?.parent)) {
19!
451
            if (isOkDeclarationLocation(statement.parent.parent.parent)) {
5✔
452
                return;
4✔
453
            }
454
        }
455

456
        //the statement was defined in the wrong place. Flag it.
457
        this.event.program.diagnostics.register({
15✔
458
            ...DiagnosticMessages.keywordMustBeDeclaredAtNamespaceLevel(keyword),
459
            location: rangeFactory ? util.createLocationFromFileRange(this.event.file, rangeFactory()) : statement.location
15!
460
        });
461
    }
462

463
    private validateFunctionParameterCount(func: FunctionExpression) {
464
        if (func.parameters.length > CallExpression.MaximumArguments) {
2,450✔
465
            //flag every parameter over the limit
466
            for (let i = CallExpression.MaximumArguments; i < func.parameters.length; i++) {
2✔
467
                this.event.program.diagnostics.register({
3✔
468
                    ...DiagnosticMessages.tooManyCallableParameters(func.parameters.length, CallExpression.MaximumArguments),
469
                    location: func.parameters[i]?.tokens.name?.location ?? func.parameters[i]?.location ?? func.location
36!
470
                });
471
            }
472
        }
473
    }
474

475
    private validateEnumDeclaration(stmt: EnumStatement) {
476
        const members = stmt.getMembers();
155✔
477
        //the enum data type is based on the first member value
478
        const enumValueKind = (members.find(x => x.value)?.value as LiteralExpression)?.tokens?.value?.kind ?? TokenKind.IntegerLiteral;
216✔
479
        const memberNames = new Set<string>();
155✔
480
        for (const member of members) {
155✔
481
            const memberNameLower = member.name?.toLowerCase();
307!
482

483
            /**
484
             * flag duplicate member names
485
             */
486
            if (memberNames.has(memberNameLower)) {
307✔
487
                this.event.program.diagnostics.register({
1✔
488
                    ...DiagnosticMessages.duplicateIdentifier(member.name),
489
                    location: member.location
490
                });
491
            } else {
492
                memberNames.add(memberNameLower);
306✔
493
            }
494

495
            //Enforce all member values are the same type
496
            this.validateEnumValueTypes(member, enumValueKind);
307✔
497
        }
498
    }
499

500
    private validateEnumValueTypes(member: EnumMemberStatement, enumValueKind: TokenKind) {
501
        let memberValueKind: TokenKind;
502
        let memberValue: Expression;
503
        if (isUnaryExpression(member.value)) {
307✔
504
            memberValueKind = (member.value?.right as LiteralExpression)?.tokens?.value?.kind;
2!
505
            memberValue = member.value?.right;
2!
506
        } else {
507
            memberValueKind = (member.value as LiteralExpression)?.tokens?.value?.kind;
305✔
508
            memberValue = member.value;
305✔
509
        }
510
        const range = (memberValue ?? member)?.location?.range;
307!
511
        if (
307✔
512
            //is integer enum, has value, that value type is not integer
513
            (enumValueKind === TokenKind.IntegerLiteral && memberValueKind && memberValueKind !== enumValueKind) ||
1,030✔
514
            //has value, that value is not a literal
515
            (memberValue && !isLiteralExpression(memberValue))
516
        ) {
517
            this.event.program.diagnostics.register({
6✔
518
                ...DiagnosticMessages.enumValueMustBeType(
519
                    enumValueKind.replace(/literal$/i, '').toLowerCase()
520
                ),
521
                location: util.createLocationFromFileRange(this.event.file, range)
522
            });
523
        }
524

525
        //is non integer value
526
        if (enumValueKind !== TokenKind.IntegerLiteral) {
307✔
527
            //default value present
528
            if (memberValueKind) {
112✔
529
                //member value is same as enum
530
                if (memberValueKind !== enumValueKind) {
110✔
531
                    this.event.program.diagnostics.register({
1✔
532
                        ...DiagnosticMessages.enumValueMustBeType(
533
                            enumValueKind.replace(/literal$/i, '').toLowerCase()
534
                        ),
535
                        location: util.createLocationFromFileRange(this.event.file, range)
536
                    });
537
                }
538

539
                //default value missing
540
            } else {
541
                this.event.program.diagnostics.register({
2✔
542
                    ...DiagnosticMessages.enumValueIsRequired(
543
                        enumValueKind.replace(/literal$/i, '').toLowerCase()
544
                    ),
545
                    location: util.createLocationFromFileRange(this.event.file, range)
546
                });
547
            }
548
        }
549
    }
550

551

552
    private validateConditionalCompileConst(ccConst: Token) {
553
        const isBool = ccConst.kind === TokenKind.True || ccConst.kind === TokenKind.False;
24✔
554
        if (!isBool && !this.event.file.ast.bsConsts.has(ccConst.text.toLowerCase())) {
24✔
555
            this.event.program.diagnostics.register({
2✔
556
                ...DiagnosticMessages.hashConstDoesNotExist(),
557
                location: ccConst.location
558
            });
559
            return false;
2✔
560
        }
561
        return true;
22✔
562
    }
563

564
    /**
565
     * Find statements defined at the top level (or inside a namespace body) that are not allowed to be there
566
     */
567
    private flagTopLevelStatements() {
568
        const statements = [...this.event.file.ast.statements];
2,140✔
569
        while (statements.length > 0) {
2,140✔
570
            const statement = statements.pop();
4,024✔
571
            if (isNamespaceStatement(statement)) {
4,024✔
572
                statements.push(...statement.body.statements);
621✔
573
            } else {
574
                //only allow these statement types
575
                if (
3,403✔
576
                    !isFunctionStatement(statement) &&
7,842✔
577
                    !isClassStatement(statement) &&
578
                    !isEnumStatement(statement) &&
579
                    !isInterfaceStatement(statement) &&
580
                    !isLibraryStatement(statement) &&
581
                    !isImportStatement(statement) &&
582
                    !isConstStatement(statement) &&
583
                    !isTypecastStatement(statement) &&
584
                    !isConditionalCompileConstStatement(statement) &&
585
                    !isConditionalCompileErrorStatement(statement) &&
586
                    !isConditionalCompileStatement(statement) &&
587
                    !isAliasStatement(statement) &&
588
                    !isTypeStatement(statement)
589
                ) {
590
                    this.event.program.diagnostics.register({
10✔
591
                        ...DiagnosticMessages.unexpectedStatementOutsideFunction(),
592
                        location: statement.location
593
                    });
594
                }
595
            }
596
        }
597
    }
598

599
    private isAllowedAtTopOfFile(statement: Statement): statement is LibraryStatement | ImportStatement | TypecastStatement | AliasStatement {
600
        return isLibraryStatement(statement) || isImportStatement(statement) || isTypecastStatement(statement) || isAliasStatement(statement);
2,303✔
601
    }
602

603
    private getTopOfFileStatements() {
604
        let topOfFileIncludeStatements = [] as Array<LibraryStatement | ImportStatement | TypecastStatement | AliasStatement>;
2,139✔
605
        for (let stmt of this.event.file.parser.ast.statements) {
2,139✔
606
            //if we found a non-library statement, this statement is not at the top of the file
607
            if (this.isAllowedAtTopOfFile(stmt)) {
2,294✔
608
                topOfFileIncludeStatements.push(stmt);
243✔
609
            } else {
610
                //break out of the loop, we found all of our library statements
611
                break;
2,051✔
612
            }
613
        }
614
        return topOfFileIncludeStatements;
2,139✔
615
    }
616

617
    private validateTopOfFileStatements() {
618
        let topOfFileStatements = this.getTopOfFileStatements();
2,139✔
619

620
        let statements = [
2,139✔
621
            // eslint-disable-next-line @typescript-eslint/dot-notation
622
            ...this.event.file['_cachedLookups'].libraryStatements,
623
            // eslint-disable-next-line @typescript-eslint/dot-notation
624
            ...this.event.file['_cachedLookups'].importStatements,
625
            // eslint-disable-next-line @typescript-eslint/dot-notation
626
            ...this.event.file['_cachedLookups'].aliasStatements
627
        ];
628
        for (let result of statements) {
2,139✔
629
            //if this statement is not one of the top-of-file statements,
630
            //then add a diagnostic explaining that it is invalid
631
            if (!topOfFileStatements.includes(result)) {
237✔
632
                if (isLibraryStatement(result)) {
5✔
633
                    this.event.program.diagnostics.register({
2✔
634
                        ...DiagnosticMessages.unexpectedStatementLocation('library', 'at the top of the file'),
635
                        location: result.location
636
                    });
637
                } else if (isImportStatement(result)) {
3✔
638
                    this.event.program.diagnostics.register({
1✔
639
                        ...DiagnosticMessages.unexpectedStatementLocation('import', 'at the top of the file'),
640
                        location: result.location
641
                    });
642
                } else if (isAliasStatement(result)) {
2!
643
                    this.event.program.diagnostics.register({
2✔
644
                        ...DiagnosticMessages.unexpectedStatementLocation('alias', 'at the top of the file'),
645
                        location: result.location
646
                    });
647
                }
648
            }
649
        }
650
    }
651

652
    private validateTypecastStatements() {
653
        // eslint-disable-next-line @typescript-eslint/dot-notation
654
        for (let typecastStmt of this.event.file['_cachedLookups'].typecastStatements) {
2,139✔
655
            let isBadTypecastObj = false;
28✔
656

657
            const block = typecastStmt.findAncestor<Body | Block>(node => (isBody(node) || isBlock(node)));
28✔
658
            const resultVarStr = util.getAllDottedGetPartsAsString(typecastStmt.typecastExpression.obj);
28✔
659
            const hasFunctionAncestor = !!typecastStmt.findAncestor(isFunctionExpression);
28✔
660
            if (!isVariableExpression(typecastStmt.typecastExpression.obj)) {
28✔
661
                isBadTypecastObj = true;
2✔
662
            } else if (!hasFunctionAncestor && resultVarStr.toLowerCase() !== 'm') {
26✔
663
                // only 'm' can be typecast outside of a function body
664
                isBadTypecastObj = true;
1✔
665
            } else if (block.getSymbolTable().hasSymbol(resultVarStr, SymbolTypeFlag.typetime)) {
25✔
666
                // can only typecast runtime symbols
667
                isBadTypecastObj = true;
1✔
668
            }
669

670
            if (isBadTypecastObj) {
28✔
671
                this.event.program.diagnostics.register({
4✔
672
                    ...DiagnosticMessages.invalidTypecastStatementApplication(resultVarStr, hasFunctionAncestor),
673
                    location: typecastStmt.typecastExpression.obj.location
674
                });
675
            }
676

677
            let isFirst = true;
28✔
678
            for (let i = 0; i < typecastStmt.statementIndex; i++) {
28✔
679
                const targetStatement = block.statements[i];
9✔
680
                // allow multiple typecast statements at the top of a block or namespace, but no other statements before them
681
                isFirst = isFirst && this.isAllowedAtTopOfFile(targetStatement);
9✔
682
                if (isTypecastStatement(targetStatement) && targetStatement !== typecastStmt) {
9✔
683
                    // do not allow multiple typecast statements that typecast the same variable, even if they are at the top of the block/namespace
684
                    const otherResultVarStr = util.getAllDottedGetPartsAsString(targetStatement.typecastExpression.obj);
4✔
685
                    if (otherResultVarStr.toLowerCase() === resultVarStr.toLowerCase()) {
4✔
686
                        isFirst = false;
3✔
687
                    }
688
                }
689
                if (!isFirst) {
9✔
690
                    break;
5✔
691
                }
692
            }
693

694
            if (!isFirst) {
28✔
695
                this.event.program.diagnostics.register({
5✔
696
                    ...DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of block or namespace'),
697
                    location: typecastStmt.location
698
                });
699
            }
700
        }
701
    }
702

703
    private validateContinueStatement(statement: ContinueStatement) {
704
        const validateLoopTypeMatch = (expectedLoopType: TokenKind) => {
8✔
705
            //coerce ForEach to For
706
            expectedLoopType = expectedLoopType === TokenKind.ForEach ? TokenKind.For : expectedLoopType;
7✔
707
            const actualLoopType = statement.tokens.loopType;
7✔
708
            if (actualLoopType && expectedLoopType?.toLowerCase() !== actualLoopType.text?.toLowerCase()) {
7!
709
                this.event.program.diagnostics.register({
3✔
710
                    location: statement.tokens.loopType.location,
711
                    ...DiagnosticMessages.expectedToken(expectedLoopType)
712
                });
713
            }
714
        };
715

716
        //find the parent loop statement
717
        const parent = statement.findAncestor<WhileStatement | ForStatement | ForEachStatement>((node) => {
8✔
718
            if (isWhileStatement(node)) {
18✔
719
                validateLoopTypeMatch(node.tokens.while.kind);
3✔
720
                return true;
3✔
721
            } else if (isForStatement(node)) {
15✔
722
                validateLoopTypeMatch(node.tokens.for.kind);
3✔
723
                return true;
3✔
724
            } else if (isForEachStatement(node)) {
12✔
725
                validateLoopTypeMatch(node.tokens.forEach.kind);
1✔
726
                return true;
1✔
727
            }
728
        });
729
        //flag continue statements found outside of a loop
730
        if (!parent) {
8✔
731
            this.event.program.diagnostics.register({
1✔
732
                location: statement.location,
733
                ...DiagnosticMessages.illegalContinueStatement()
734
            });
735
        }
736
    }
737

738
    /**
739
     * Validate that there are no optional chaining operators on the left-hand-side of an assignment, indexed set, or dotted get
740
     */
741
    private validateNoOptionalChainingInVarSet(parent: AstNode, children: AstNode[]) {
742
        const nodes = [...children, parent];
122✔
743
        //flag optional chaining anywhere in the left of this statement
744
        while (nodes.length > 0) {
122✔
745
            const node = nodes.shift();
244✔
746
            if (
244✔
747
                // a?.b = true or a.b?.c = true
748
                ((isDottedSetStatement(node) || isDottedGetExpression(node)) && node.tokens.dot?.kind === TokenKind.QuestionDot) ||
1,390!
749
                // a.b?[2] = true
750
                (isIndexedGetExpression(node) && (node?.tokens.questionDot?.kind === TokenKind.QuestionDot || node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare)) ||
36!
751
                // a?[1] = true
752
                (isIndexedSetStatement(node) && node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare)
57!
753
            ) {
754
                //try to highlight the entire left-hand-side expression if possible
755
                let range: Range;
756
                if (isDottedSetStatement(parent)) {
8✔
757
                    range = util.createBoundingRange(parent.obj?.location, parent.tokens.dot, parent.tokens.name);
5!
758
                } else if (isIndexedSetStatement(parent)) {
3!
759
                    range = util.createBoundingRange(parent.obj?.location, parent.tokens.openingSquare, ...parent.indexes, parent.tokens.closingSquare);
3!
760
                } else {
UNCOV
761
                    range = node.location?.range;
×
762
                }
763

764
                this.event.program.diagnostics.register({
8✔
765
                    ...DiagnosticMessages.noOptionalChainingInLeftHandSideOfAssignment(),
766
                    location: util.createLocationFromFileRange(this.event.file, range)
767
                });
768
            }
769

770
            if (node === parent) {
244✔
771
                break;
122✔
772
            } else {
773
                nodes.push(node.parent);
122✔
774
            }
775
        }
776
    }
777

778
    private setUpComplementSymbolTables(node: IfStatement | ConditionalCompileStatement, predicate: (node: AstNode) => boolean) {
779
        if (isBlock(node.elseBranch)) {
156✔
780
            const elseTable = node.elseBranch.symbolTable;
28✔
781
            let currentNode = node;
28✔
782
            while (predicate(currentNode)) {
28✔
783
                const thenBranch = (currentNode as IfStatement | ConditionalCompileStatement).thenBranch;
39✔
784
                elseTable.complementOtherTable(thenBranch.symbolTable);
39✔
785
                currentNode = currentNode.parent as IfStatement | ConditionalCompileStatement;
39✔
786
            }
787
        }
788
    }
789
}
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