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

rokucommunity / brighterscript / #15028

13 Dec 2025 01:37PM UTC coverage: 87.29% (-0.006%) from 87.296%
#15028

push

web-flow
Merge 549ba37ce into a65ebfcad

14407 of 17439 branches covered (82.61%)

Branch coverage included in aggregate %.

36 of 36 new or added lines in 4 files covered. (100.0%)

28 existing lines in 4 files now uncovered.

15090 of 16353 relevant lines covered (92.28%)

24234.91 hits per line

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

85.41
/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, 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, 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, Body, WhileStatement, TypecastStatement, Block, AliasStatement, IfStatement, ConditionalCompileStatement } from '../../parser/Statement';
11
import { SymbolTypeFlag } from '../../SymbolTypeFlag';
1✔
12
import { ArrayDefaultTypeReferenceType } from '../../types/ReferenceType';
1✔
13
import { AssociativeArrayType } from '../../types/AssociativeArrayType';
1✔
14
import { DynamicType } from '../../types/DynamicType';
1✔
15
import util from '../../util';
1✔
16
import type { Range } from 'vscode-languageserver';
17
import type { Token } from '../../lexer/Token';
18
import type { BrightScriptDoc } from '../../parser/BrightScriptDocParser';
19
import brsDocParser from '../../parser/BrightScriptDocParser';
1✔
20
import { TypeStatementType } from '../../types/TypeStatementType';
1✔
21

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

28

29
    public process() {
30
        const unlinkGlobalSymbolTable = this.event.file.parser.symbolTable.pushParentProvider(() => this.event.program.globalScope.symbolTable);
8,168✔
31

32
        util.validateTooDeepFile(this.event.file);
2,037✔
33

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

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

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

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

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

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

80
                this.validateEnumDeclaration(node);
155✔
81

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

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

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

106
                    this.event.file.parser.ast.symbolTable.addSymbol(
133✔
107
                        transpiledClassConstructor,
108
                        { definingNode: node },
109
                        node.getConstructorType(),
110
                        // eslint-disable-next-line no-bitwise
111
                        SymbolTypeFlag.runtime | SymbolTypeFlag.postTranspile
112
                    );
113
                }
114
            },
115
            AssignmentStatement: (node) => {
116
                if (!node?.tokens?.name) {
812!
117
                    return;
×
118
                }
119
                const data: ExtraSymbolData = {};
812✔
120
                //register this variable
121
                let nodeType = node.getType({ flags: SymbolTypeFlag.runtime, data: data });
812✔
122
                if (isInvalidType(nodeType) || isVoidType(nodeType)) {
812✔
123
                    nodeType = DynamicType.instance;
11✔
124
                }
125
                node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true, isFromDocComment: data.isFromDocComment, isFromCallFunc: data.isFromCallFunc }, nodeType, SymbolTypeFlag.runtime);
812!
126
            },
127
            DottedSetStatement: (node) => {
128
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
102✔
129
            },
130
            IndexedSetStatement: (node) => {
131
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
19✔
132
            },
133
            ForEachStatement: (node) => {
134
                //register the for loop variable
135
                const loopTargetType = node.target.getType({ flags: SymbolTypeFlag.runtime });
28✔
136
                const loopVarType = new ArrayDefaultTypeReferenceType(loopTargetType);
28✔
137

138
                node.parent.getSymbolTable()?.addSymbol(node.tokens.item.text, { definingNode: node, isInstance: true, canUseInDefinedAstNode: true }, loopVarType, SymbolTypeFlag.runtime);
28!
139
            },
140
            NamespaceStatement: (node) => {
141
                if (!node?.nameExpression) {
622!
142
                    return;
×
143
                }
144
                this.validateDeclarationLocations(node, 'namespace', () => util.createBoundingRange(node.tokens.namespace, node.nameExpression));
622✔
145
                //Namespace Types are added at the Scope level - This is handled when the SymbolTables get linked
146
            },
147
            FunctionStatement: (node) => {
148
                this.validateDeclarationLocations(node, 'function', () => util.createBoundingRange(node.func.tokens.functionType, node.tokens.name));
2,036✔
149
                const funcType = node.getType({ flags: SymbolTypeFlag.typetime });
2,036✔
150

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

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

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

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

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

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

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

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

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

334
            },
335
            TypeStatement: (node) => {
336
                this.validateDeclarationLocations(node, 'type', () => util.createBoundingRange(node.tokens.type, node.tokens.name));
20✔
337
                const wrappedNodeType = node.getType({ flags: SymbolTypeFlag.runtime });
20✔
338
                const typeStmtType = new TypeStatementType(node.tokens.name.text, wrappedNodeType);
20✔
339
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, isFromTypeStatement: true }, typeStmtType, SymbolTypeFlag.typetime);
20✔
340

341
            },
342
            IfStatement: (node) => {
343
                this.setUpComplementSymbolTables(node, isIfStatement);
152✔
344
            },
345
            Block: (node) => {
346
                const blockSymbolTable = node.symbolTable;
2,605✔
347
                if (node.findAncestor<Block>(isFunctionExpression)) {
2,605✔
348
                    // this block is in a function. order matters!
349
                    blockSymbolTable.isOrdered = true;
2,601✔
350
                }
351
                if (!isFunctionExpression(node.parent) && node.parent) {
2,605✔
352
                    node.symbolTable.name = `Block-${node.parent.kind}@${node.location?.range?.start?.line}`;
288✔
353
                    // we're a block inside another block (or body). This block is a pocket in the bigger block
354
                    node.parent.getSymbolTable().addPocketTable({
288✔
355
                        index: node.parent.statementIndex,
356
                        table: node.symbolTable,
357
                        // code always flows through ConditionalCompiles, because we walk according to defined BSConsts
358
                        willAlwaysBeExecuted: isConditionalCompileStatement(node.parent)
359
                    });
360
                }
361
            },
362
            AstNode: (node) => {
363
                //check for doc comments
364
                if (!node.leadingTrivia || node.leadingTrivia.length === 0) {
28,416✔
365
                    return;
4,988✔
366
                }
367
                const doc = brsDocParser.parseNode(node);
23,428✔
368
                if (doc.tags.length === 0) {
23,428✔
369
                    return;
23,376✔
370
                }
371

372
                let funcExpr = node.findAncestor<FunctionExpression>(isFunctionExpression);
52✔
373
                if (funcExpr) {
52✔
374
                    // handle comment tags inside a function expression
375
                    this.processDocTagsInFunction(doc, node, funcExpr);
8✔
376
                } else {
377
                    //handle comment tags outside of a function expression
378
                    this.processDocTagsAtTopLevel(doc, node);
44✔
379
                }
380
            }
381
        });
382

383
        this.event.file.ast.walk((node, parent) => {
2,037✔
384
            visitor(node, parent);
28,416✔
385
        }, {
386
            walkMode: WalkMode.visitAllRecursive
387
        });
388
    }
389

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

393
        // For example, declaring variable types:
394
        // const symbolTable = funcExpr.body.getSymbolTable();
395

396
        // for (const varTag of doc.getAllTags(BrsDocTagKind.Var)) {
397
        //     const varName = (varTag as BrsDocParamTag).name;
398
        //     const varTypeStr = (varTag as BrsDocParamTag).type;
399
        //     const data: ExtraSymbolData = {};
400
        //     const type = doc.getTypeFromContext(varTypeStr, node, { flags: SymbolTypeFlag.typetime, fullName: varTypeStr, data: data, tableProvider: () => symbolTable });
401
        //     if (type) {
402
        //         symbolTable.addSymbol(varName, { ...data, isFromDocComment: true }, type, SymbolTypeFlag.runtime);
403
        //     }
404
        // }
405
    }
406

407
    private processDocTagsAtTopLevel(doc: BrightScriptDoc, node: AstNode) {
408
        //TODO:
409
        // - handle import statements?
410
        // - handle library statements?
411
        // - handle typecast statements?
412
        // - handle alias statements?
413
        // - handle const statements?
414
        // - allow interface definitions?
415
    }
416

417
    /**
418
     * Validate that a statement is defined in one of these specific locations
419
     *  - the root of the AST
420
     *  - inside a namespace
421
     * This is applicable to things like FunctionStatement, ClassStatement, NamespaceStatement, EnumStatement, InterfaceStatement
422
     */
423
    private validateDeclarationLocations(statement: Statement, keyword: string, rangeFactory?: () => (Range | undefined)) {
424
        //if nested inside a namespace, or defined at the root of the AST (i.e. in a body that has no parent)
425
        const isOkDeclarationLocation = (parentNode) => {
3,599✔
426
            return isNamespaceStatement(parentNode?.parent) || (isBody(parentNode) && !parentNode?.parent);
3,604!
427
        };
428
        if (isOkDeclarationLocation(statement.parent)) {
3,599✔
429
            return;
3,580✔
430
        }
431

432
        // is this in a top levelconditional compile?
433
        if (isConditionalCompileStatement(statement.parent?.parent)) {
19!
434
            if (isOkDeclarationLocation(statement.parent.parent.parent)) {
5✔
435
                return;
4✔
436
            }
437
        }
438

439
        //the statement was defined in the wrong place. Flag it.
440
        this.event.program.diagnostics.register({
15✔
441
            ...DiagnosticMessages.keywordMustBeDeclaredAtNamespaceLevel(keyword),
442
            location: rangeFactory ? util.createLocationFromFileRange(this.event.file, rangeFactory()) : statement.location
15!
443
        });
444
    }
445

446
    private validateFunctionParameterCount(func: FunctionExpression) {
447
        if (func.parameters.length > CallExpression.MaximumArguments) {
2,317✔
448
            //flag every parameter over the limit
449
            for (let i = CallExpression.MaximumArguments; i < func.parameters.length; i++) {
2✔
450
                this.event.program.diagnostics.register({
3✔
451
                    ...DiagnosticMessages.tooManyCallableParameters(func.parameters.length, CallExpression.MaximumArguments),
452
                    location: func.parameters[i]?.tokens.name?.location ?? func.parameters[i]?.location ?? func.location
36!
453
                });
454
            }
455
        }
456
    }
457

458
    private validateEnumDeclaration(stmt: EnumStatement) {
459
        const members = stmt.getMembers();
155✔
460
        //the enum data type is based on the first member value
461
        const enumValueKind = (members.find(x => x.value)?.value as LiteralExpression)?.tokens?.value?.kind ?? TokenKind.IntegerLiteral;
216✔
462
        const memberNames = new Set<string>();
155✔
463
        for (const member of members) {
155✔
464
            const memberNameLower = member.name?.toLowerCase();
307!
465

466
            /**
467
             * flag duplicate member names
468
             */
469
            if (memberNames.has(memberNameLower)) {
307✔
470
                this.event.program.diagnostics.register({
1✔
471
                    ...DiagnosticMessages.duplicateIdentifier(member.name),
472
                    location: member.location
473
                });
474
            } else {
475
                memberNames.add(memberNameLower);
306✔
476
            }
477

478
            //Enforce all member values are the same type
479
            this.validateEnumValueTypes(member, enumValueKind);
307✔
480
        }
481
    }
482

483
    private validateEnumValueTypes(member: EnumMemberStatement, enumValueKind: TokenKind) {
484
        let memberValueKind: TokenKind;
485
        let memberValue: Expression;
486
        if (isUnaryExpression(member.value)) {
307✔
487
            memberValueKind = (member.value?.right as LiteralExpression)?.tokens?.value?.kind;
2!
488
            memberValue = member.value?.right;
2!
489
        } else {
490
            memberValueKind = (member.value as LiteralExpression)?.tokens?.value?.kind;
305✔
491
            memberValue = member.value;
305✔
492
        }
493
        const range = (memberValue ?? member)?.location?.range;
307!
494
        if (
307✔
495
            //is integer enum, has value, that value type is not integer
496
            (enumValueKind === TokenKind.IntegerLiteral && memberValueKind && memberValueKind !== enumValueKind) ||
1,030✔
497
            //has value, that value is not a literal
498
            (memberValue && !isLiteralExpression(memberValue))
499
        ) {
500
            this.event.program.diagnostics.register({
6✔
501
                ...DiagnosticMessages.enumValueMustBeType(
502
                    enumValueKind.replace(/literal$/i, '').toLowerCase()
503
                ),
504
                location: util.createLocationFromFileRange(this.event.file, range)
505
            });
506
        }
507

508
        //is non integer value
509
        if (enumValueKind !== TokenKind.IntegerLiteral) {
307✔
510
            //default value present
511
            if (memberValueKind) {
112✔
512
                //member value is same as enum
513
                if (memberValueKind !== enumValueKind) {
110✔
514
                    this.event.program.diagnostics.register({
1✔
515
                        ...DiagnosticMessages.enumValueMustBeType(
516
                            enumValueKind.replace(/literal$/i, '').toLowerCase()
517
                        ),
518
                        location: util.createLocationFromFileRange(this.event.file, range)
519
                    });
520
                }
521

522
                //default value missing
523
            } else {
524
                this.event.program.diagnostics.register({
2✔
525
                    ...DiagnosticMessages.enumValueIsRequired(
526
                        enumValueKind.replace(/literal$/i, '').toLowerCase()
527
                    ),
528
                    location: util.createLocationFromFileRange(this.event.file, range)
529
                });
530
            }
531
        }
532
    }
533

534

535
    private validateConditionalCompileConst(ccConst: Token) {
536
        const isBool = ccConst.kind === TokenKind.True || ccConst.kind === TokenKind.False;
24✔
537
        if (!isBool && !this.event.file.ast.bsConsts.has(ccConst.text.toLowerCase())) {
24✔
538
            this.event.program.diagnostics.register({
2✔
539
                ...DiagnosticMessages.hashConstDoesNotExist(),
540
                location: ccConst.location
541
            });
542
            return false;
2✔
543
        }
544
        return true;
22✔
545
    }
546

547
    /**
548
     * Find statements defined at the top level (or inside a namespace body) that are not allowed to be there
549
     */
550
    private flagTopLevelStatements() {
551
        const statements = [...this.event.file.ast.statements];
2,037✔
552
        while (statements.length > 0) {
2,037✔
553
            const statement = statements.pop();
3,844✔
554
            if (isNamespaceStatement(statement)) {
3,844✔
555
                statements.push(...statement.body.statements);
617✔
556
            } else {
557
                //only allow these statement types
558
                if (
3,227✔
559
                    !isFunctionStatement(statement) &&
7,333✔
560
                    !isClassStatement(statement) &&
561
                    !isEnumStatement(statement) &&
562
                    !isInterfaceStatement(statement) &&
563
                    !isLibraryStatement(statement) &&
564
                    !isImportStatement(statement) &&
565
                    !isConstStatement(statement) &&
566
                    !isTypecastStatement(statement) &&
567
                    !isConditionalCompileConstStatement(statement) &&
568
                    !isConditionalCompileErrorStatement(statement) &&
569
                    !isConditionalCompileStatement(statement) &&
570
                    !isAliasStatement(statement) &&
571
                    !isTypeStatement(statement)
572
                ) {
573
                    this.event.program.diagnostics.register({
9✔
574
                        ...DiagnosticMessages.unexpectedStatementOutsideFunction(),
575
                        location: statement.location
576
                    });
577
                }
578
            }
579
        }
580
    }
581

582
    private getTopOfFileStatements() {
583
        let topOfFileIncludeStatements = [] as Array<LibraryStatement | ImportStatement | TypecastStatement | AliasStatement>;
4,072✔
584
        for (let stmt of this.event.file.parser.ast.statements) {
4,072✔
585
            //if we found a non-library statement, this statement is not at the top of the file
586
            if (isLibraryStatement(stmt) || isImportStatement(stmt) || isTypecastStatement(stmt) || isAliasStatement(stmt)) {
4,378✔
587
                topOfFileIncludeStatements.push(stmt);
482✔
588
            } else {
589
                //break out of the loop, we found all of our library statements
590
                break;
3,896✔
591
            }
592
        }
593
        return topOfFileIncludeStatements;
4,072✔
594
    }
595

596
    private validateTopOfFileStatements() {
597
        let topOfFileStatements = this.getTopOfFileStatements();
2,036✔
598

599
        let statements = [
2,036✔
600
            // eslint-disable-next-line @typescript-eslint/dot-notation
601
            ...this.event.file['_cachedLookups'].libraryStatements,
602
            // eslint-disable-next-line @typescript-eslint/dot-notation
603
            ...this.event.file['_cachedLookups'].importStatements,
604
            // eslint-disable-next-line @typescript-eslint/dot-notation
605
            ...this.event.file['_cachedLookups'].aliasStatements
606
        ];
607
        for (let result of statements) {
2,036✔
608
            //if this statement is not one of the top-of-file statements,
609
            //then add a diagnostic explaining that it is invalid
610
            if (!topOfFileStatements.includes(result)) {
236✔
611
                if (isLibraryStatement(result)) {
5✔
612
                    this.event.program.diagnostics.register({
2✔
613
                        ...DiagnosticMessages.unexpectedStatementLocation('library', 'at the top of the file'),
614
                        location: result.location
615
                    });
616
                } else if (isImportStatement(result)) {
3✔
617
                    this.event.program.diagnostics.register({
1✔
618
                        ...DiagnosticMessages.unexpectedStatementLocation('import', 'at the top of the file'),
619
                        location: result.location
620
                    });
621
                } else if (isAliasStatement(result)) {
2!
622
                    this.event.program.diagnostics.register({
2✔
623
                        ...DiagnosticMessages.unexpectedStatementLocation('alias', 'at the top of the file'),
624
                        location: result.location
625
                    });
626
                }
627
            }
628
        }
629
    }
630

631
    private validateTypecastStatements() {
632
        let topOfFileTypecastStatements = this.getTopOfFileStatements().filter(stmt => isTypecastStatement(stmt));
2,036✔
633

634
        //check only one `typecast` statement at "top" of file (eg. before non import/library statements)
635
        for (let i = 1; i < topOfFileTypecastStatements.length; i++) {
2,036✔
636
            const typecastStmt = topOfFileTypecastStatements[i];
1✔
637
            this.event.program.diagnostics.register({
1✔
638
                ...DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of function or namespace'),
639
                location: typecastStmt.location
640
            });
641
        }
642

643
        // eslint-disable-next-line @typescript-eslint/dot-notation
644
        for (let result of this.event.file['_cachedLookups'].typecastStatements) {
2,036✔
645
            let isBadTypecastObj = false;
22✔
646
            if (!isVariableExpression(result.typecastExpression.obj)) {
22✔
647
                isBadTypecastObj = true;
1✔
648
            } else if (result.typecastExpression.obj.tokens.name.text.toLowerCase() !== 'm') {
21✔
649
                isBadTypecastObj = true;
1✔
650
            }
651
            if (isBadTypecastObj) {
22✔
652
                this.event.program.diagnostics.register({
2✔
653
                    ...DiagnosticMessages.invalidTypecastStatementApplication(util.getAllDottedGetPartsAsString(result.typecastExpression.obj)),
654
                    location: result.typecastExpression.obj.location
655
                });
656
            }
657

658
            if (topOfFileTypecastStatements.includes(result)) {
22✔
659
                // already validated
660
                continue;
10✔
661
            }
662

663
            const block = result.findAncestor<Body | Block>(node => (isBody(node) || isBlock(node)));
12✔
664
            const isFirst = block?.statements[0] === result;
12!
665
            const isAllowedBlock = (isBody(block) || isFunctionExpression(block.parent) || isNamespaceStatement(block.parent));
12!
666

667
            if (!isFirst || !isAllowedBlock) {
12✔
668
                this.event.program.diagnostics.register({
3✔
669
                    ...DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of function or namespace'),
670
                    location: result.location
671
                });
672
            }
673
        }
674
    }
675

676
    private validateContinueStatement(statement: ContinueStatement) {
677
        const validateLoopTypeMatch = (expectedLoopType: TokenKind) => {
8✔
678
            //coerce ForEach to For
679
            expectedLoopType = expectedLoopType === TokenKind.ForEach ? TokenKind.For : expectedLoopType;
7✔
680
            const actualLoopType = statement.tokens.loopType;
7✔
681
            if (actualLoopType && expectedLoopType?.toLowerCase() !== actualLoopType.text?.toLowerCase()) {
7!
682
                this.event.program.diagnostics.register({
3✔
683
                    location: statement.tokens.loopType.location,
684
                    ...DiagnosticMessages.expectedToken(expectedLoopType)
685
                });
686
            }
687
        };
688

689
        //find the parent loop statement
690
        const parent = statement.findAncestor<WhileStatement | ForStatement | ForEachStatement>((node) => {
8✔
691
            if (isWhileStatement(node)) {
18✔
692
                validateLoopTypeMatch(node.tokens.while.kind);
3✔
693
                return true;
3✔
694
            } else if (isForStatement(node)) {
15✔
695
                validateLoopTypeMatch(node.tokens.for.kind);
3✔
696
                return true;
3✔
697
            } else if (isForEachStatement(node)) {
12✔
698
                validateLoopTypeMatch(node.tokens.forEach.kind);
1✔
699
                return true;
1✔
700
            }
701
        });
702
        //flag continue statements found outside of a loop
703
        if (!parent) {
8✔
704
            this.event.program.diagnostics.register({
1✔
705
                location: statement.location,
706
                ...DiagnosticMessages.illegalContinueStatement()
707
            });
708
        }
709
    }
710

711
    /**
712
     * Validate that there are no optional chaining operators on the left-hand-side of an assignment, indexed set, or dotted get
713
     */
714
    private validateNoOptionalChainingInVarSet(parent: AstNode, children: AstNode[]) {
715
        const nodes = [...children, parent];
121✔
716
        //flag optional chaining anywhere in the left of this statement
717
        while (nodes.length > 0) {
121✔
718
            const node = nodes.shift();
242✔
719
            if (
242✔
720
                // a?.b = true or a.b?.c = true
721
                ((isDottedSetStatement(node) || isDottedGetExpression(node)) && node.tokens.dot?.kind === TokenKind.QuestionDot) ||
1,379!
722
                // a.b?[2] = true
723
                (isIndexedGetExpression(node) && (node?.tokens.questionDot?.kind === TokenKind.QuestionDot || node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare)) ||
36!
724
                // a?[1] = true
725
                (isIndexedSetStatement(node) && node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare)
57!
726
            ) {
727
                //try to highlight the entire left-hand-side expression if possible
728
                let range: Range;
729
                if (isDottedSetStatement(parent)) {
8✔
730
                    range = util.createBoundingRange(parent.obj?.location, parent.tokens.dot, parent.tokens.name);
5!
731
                } else if (isIndexedSetStatement(parent)) {
3!
732
                    range = util.createBoundingRange(parent.obj?.location, parent.tokens.openingSquare, ...parent.indexes, parent.tokens.closingSquare);
3!
733
                } else {
UNCOV
734
                    range = node.location?.range;
×
735
                }
736

737
                this.event.program.diagnostics.register({
8✔
738
                    ...DiagnosticMessages.noOptionalChainingInLeftHandSideOfAssignment(),
739
                    location: util.createLocationFromFileRange(this.event.file, range)
740
                });
741
            }
742

743
            if (node === parent) {
242✔
744
                break;
121✔
745
            } else {
746
                nodes.push(node.parent);
121✔
747
            }
748
        }
749
    }
750

751
    private setUpComplementSymbolTables(node: IfStatement | ConditionalCompileStatement, predicate: (node: AstNode) => boolean) {
752
        if (isBlock(node.elseBranch)) {
152✔
753
            const elseTable = node.elseBranch.symbolTable;
28✔
754
            let currentNode = node;
28✔
755
            while (predicate(currentNode)) {
28✔
756
                const thenBranch = (currentNode as IfStatement | ConditionalCompileStatement).thenBranch;
39✔
757
                elseTable.complementOtherTable(thenBranch.symbolTable);
39✔
758
                currentNode = currentNode.parent as IfStatement | ConditionalCompileStatement;
39✔
759
            }
760
        }
761
    }
762
}
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

© 2025 Coveralls, Inc