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

rokucommunity / brighterscript / #14403

14 May 2025 05:45PM UTC coverage: 87.003% (-2.0%) from 89.017%
#14403

push

web-flow
Merge 343773173 into a194c3925

13887 of 16869 branches covered (82.32%)

Branch coverage included in aggregate %.

174 of 191 new or added lines in 11 files covered. (91.1%)

839 existing lines in 51 files now uncovered.

14750 of 16046 relevant lines covered (91.92%)

21898.44 hits per line

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

87.01
/src/bscPlugin/validation/BrsFileValidator.ts
1
import { isAliasStatement, isArrayType, 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, 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

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

27

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

31
        util.validateTooDeepFile(this.event.file);
1,929✔
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();
1,929✔
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());
1,929✔
40

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

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

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

59
        const visitor = createVisitor({
1,929✔
60
            MethodStatement: (node) => {
61
                //add the `super` symbol to class methods
62
                if (isClassStatement(node.parent) && node.parent.hasParentClass()) {
242✔
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) {
62✔
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));
150✔
78

79
                this.validateEnumDeclaration(node);
150✔
80

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

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

95
                if (node.findAncestor(isNamespaceStatement)) {
418✔
96
                    //add the transpiled name for namespaced constructors to the root symbol table
97
                    const transpiledClassConstructor = node.getName(ParseMode.BrightScript);
129✔
98

99
                    this.event.file.parser.ast.symbolTable.addSymbol(
129✔
100
                        transpiledClassConstructor,
101
                        { definingNode: node },
102
                        node.getConstructorType(),
103
                        // eslint-disable-next-line no-bitwise
104
                        SymbolTypeFlag.runtime | SymbolTypeFlag.postTranspile
105
                    );
106
                }
107
            },
108
            AssignmentStatement: (node) => {
109
                const data: ExtraSymbolData = {};
762✔
110
                //register this variable
111
                let nodeType = node.getType({ flags: SymbolTypeFlag.runtime, data: data });
762✔
112
                if (isInvalidType(nodeType) || isVoidType(nodeType)) {
762✔
113
                    nodeType = DynamicType.instance;
11✔
114
                }
115
                node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true, isFromDocComment: data.isFromDocComment, isFromCallFunc: data.isFromCallFunc }, nodeType, SymbolTypeFlag.runtime);
762!
116
            },
117
            DottedSetStatement: (node) => {
118
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
100✔
119
            },
120
            IndexedSetStatement: (node) => {
121
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
19✔
122
            },
123
            ForEachStatement: (node) => {
124
                //register the for loop variable
125
                const loopTargetType = node.target.getType({ flags: SymbolTypeFlag.runtime });
26✔
126
                let loopVarType = isArrayType(loopTargetType) ? loopTargetType.defaultType : DynamicType.instance;
26✔
127

128
                if (!loopTargetType.isResolvable()) {
26✔
129
                    loopVarType = new ArrayDefaultTypeReferenceType(loopTargetType);
1✔
130
                }
131
                node.parent.getSymbolTable()?.addSymbol(node.tokens.item.text, { definingNode: node, isInstance: true, canUseInDefinedAstNode: true }, loopVarType, SymbolTypeFlag.runtime);
26!
132
            },
133
            NamespaceStatement: (node) => {
134
                this.validateDeclarationLocations(node, 'namespace', () => util.createBoundingRange(node.tokens.namespace, node.nameExpression));
601✔
135
                //Namespace Types are added at the Scope level - This is handled when the SymbolTables get linked
136
            },
137
            FunctionStatement: (node) => {
138
                this.validateDeclarationLocations(node, 'function', () => util.createBoundingRange(node.func.tokens.functionType, node.tokens.name));
1,928✔
139
                const funcType = node.getType({ flags: SymbolTypeFlag.typetime });
1,928✔
140

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

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

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

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

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

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

232
                    //no other expressions are allowed here
233
                } else {
UNCOV
234
                    this.event.program.diagnostics.register({
×
235
                        ...DiagnosticMessages.expectedExceptionVarToFollowCatch(),
236
                        location: node.exceptionVariableExpression?.location ?? node.tokens.catch?.location
×
237
                    });
238
                }
239
            },
240
            DimStatement: (node) => {
241
                if (node.tokens.name) {
18!
242
                    node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true }, node.getType({ flags: SymbolTypeFlag.runtime }), SymbolTypeFlag.runtime);
18✔
243
                }
244
            },
245
            ReturnStatement: (node) => {
246
                const func = node.findAncestor<FunctionExpression>(isFunctionExpression);
462✔
247
                //these situations cannot have a value next to `return`
248
                if (
462✔
249
                    //`function as void`, `sub as void`
250
                    (isVariableExpression(func?.returnTypeExpression?.expression) && func.returnTypeExpression.expression.tokens.name.text?.toLowerCase() === 'void') ||
4,902!
251
                    //`sub` <without return value>
252
                    (func.tokens.functionType?.kind === TokenKind.Sub && !func.returnTypeExpression)
1,347!
253
                ) {
254
                    //there may not be a return value
255
                    if (node.value) {
19✔
256
                        this.event.program.diagnostics.register({
11✔
257
                            ...DiagnosticMessages.voidFunctionMayNotReturnValue(func.tokens.functionType?.text),
33!
258
                            location: node.location
259
                        });
260
                    }
261

262
                } else {
263
                    //there MUST be a return value
264
                    if (!node.value) {
443✔
265
                        this.event.program.diagnostics.register({
11✔
266
                            ...DiagnosticMessages.nonVoidFunctionMustReturnValue(func?.tokens.functionType?.text),
66!
267
                            location: node.location
268
                        });
269
                    }
270
                }
271
            },
272
            ContinueStatement: (node) => {
273
                this.validateContinueStatement(node);
8✔
274
            },
275
            TypecastStatement: (node) => {
276
                node.parent.getSymbolTable().addSymbol('m', { definingNode: node, doNotMerge: true, isInstance: true }, node.getType({ flags: SymbolTypeFlag.typetime }), SymbolTypeFlag.runtime);
22✔
277
            },
278
            ConditionalCompileConstStatement: (node) => {
279
                const assign = node.assignment;
10✔
280
                const constNameLower = assign.tokens.name?.text.toLowerCase();
10!
281
                const astBsConsts = this.event.file.ast.bsConsts;
10✔
282
                if (isLiteralExpression(assign.value)) {
10!
283
                    astBsConsts.set(constNameLower, assign.value.tokens.value.text.toLowerCase() === 'true');
10✔
UNCOV
284
                } else if (isVariableExpression(assign.value)) {
×
UNCOV
285
                    if (this.validateConditionalCompileConst(assign.value.tokens.name)) {
×
UNCOV
286
                        astBsConsts.set(constNameLower, astBsConsts.get(assign.value.tokens.name.text.toLowerCase()));
×
287
                    }
288
                }
289
            },
290
            ConditionalCompileStatement: (node) => {
291
                this.validateConditionalCompileConst(node.tokens.condition);
24✔
292
            },
293
            ConditionalCompileErrorStatement: (node) => {
294
                this.event.program.diagnostics.register({
1✔
295
                    ...DiagnosticMessages.hashError(node.tokens.message.text),
296
                    location: node.location
297
                });
298
            },
299
            AliasStatement: (node) => {
300
                // eslint-disable-next-line no-bitwise
301
                const targetType = node.value.getType({ flags: SymbolTypeFlag.typetime | SymbolTypeFlag.runtime });
30✔
302

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

306
            },
307
            IfStatement: (node) => {
308
                this.setUpComplementSymbolTables(node, isIfStatement);
137✔
309
            },
310
            Block: (node) => {
311
                const blockSymbolTable = node.symbolTable;
2,464✔
312
                if (node.findAncestor<Block>(isFunctionExpression)) {
2,464✔
313
                    // this block is in a function. order matters!
314
                    blockSymbolTable.isOrdered = true;
2,460✔
315
                }
316
                if (!isFunctionExpression(node.parent)) {
2,464✔
317
                    // we're a block inside another block (or body). This block is a pocket in the bigger block
318
                    node.parent.getSymbolTable().addPocketTable({
257✔
319
                        index: node.parent.statementIndex,
320
                        table: node.symbolTable,
321
                        // code always flows through ConditionalCompiles, because we walk according to defined BSConsts
322
                        isMandatory: isConditionalCompileStatement(node.parent)
323
                    });
324
                }
325
            },
326
            AstNode: (node) => {
327
                //check for doc comments
328
                if (!node.leadingTrivia || node.leadingTrivia.length === 0) {
26,741✔
329
                    return;
4,679✔
330
                }
331
                const doc = brsDocParser.parseNode(node);
22,062✔
332
                if (doc.tags.length === 0) {
22,062✔
333
                    return;
22,014✔
334
                }
335

336
                let funcExpr = node.findAncestor<FunctionExpression>(isFunctionExpression);
48✔
337
                if (funcExpr) {
48✔
338
                    // handle comment tags inside a function expression
339
                    this.processDocTagsInFunction(doc, node, funcExpr);
8✔
340
                } else {
341
                    //handle comment tags outside of a function expression
342
                    this.processDocTagsAtTopLevel(doc, node);
40✔
343
                }
344
            }
345
        });
346

347
        this.event.file.ast.walk((node, parent) => {
1,929✔
348
            visitor(node, parent);
26,741✔
349
        }, {
350
            walkMode: WalkMode.visitAllRecursive
351
        });
352
    }
353

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

357
        // For example, declaring variable types:
358
        // const symbolTable = funcExpr.body.getSymbolTable();
359

360
        // for (const varTag of doc.getAllTags(BrsDocTagKind.Var)) {
361
        //     const varName = (varTag as BrsDocParamTag).name;
362
        //     const varTypeStr = (varTag as BrsDocParamTag).type;
363
        //     const data: ExtraSymbolData = {};
364
        //     const type = doc.getTypeFromContext(varTypeStr, node, { flags: SymbolTypeFlag.typetime, fullName: varTypeStr, data: data, tableProvider: () => symbolTable });
365
        //     if (type) {
366
        //         symbolTable.addSymbol(varName, { ...data, isFromDocComment: true }, type, SymbolTypeFlag.runtime);
367
        //     }
368
        // }
369
    }
370

371
    private processDocTagsAtTopLevel(doc: BrightScriptDoc, node: AstNode) {
372
        //TODO:
373
        // - handle import statements?
374
        // - handle library statements?
375
        // - handle typecast statements?
376
        // - handle alias statements?
377
        // - handle const statements?
378
        // - allow interface definitions?
379
    }
380

381
    /**
382
     * Validate that a statement is defined in one of these specific locations
383
     *  - the root of the AST
384
     *  - inside a namespace
385
     * This is applicable to things like FunctionStatement, ClassStatement, NamespaceStatement, EnumStatement, InterfaceStatement
386
     */
387
    private validateDeclarationLocations(statement: Statement, keyword: string, rangeFactory?: () => (Range | undefined)) {
388
        //if nested inside a namespace, or defined at the root of the AST (i.e. in a body that has no parent)
389
        const isOkDeclarationLocation = (parentNode) => {
3,392✔
390
            return isNamespaceStatement(parentNode?.parent) || (isBody(parentNode) && !parentNode?.parent);
3,397!
391
        };
392
        if (isOkDeclarationLocation(statement.parent)) {
3,392✔
393
            return;
3,374✔
394
        }
395

396
        // is this in a top levelconditional compile?
397
        if (isConditionalCompileStatement(statement.parent?.parent)) {
18!
398
            if (isOkDeclarationLocation(statement.parent.parent.parent)) {
5✔
399
                return;
4✔
400
            }
401
        }
402

403
        //the statement was defined in the wrong place. Flag it.
404
        this.event.program.diagnostics.register({
14✔
405
            ...DiagnosticMessages.keywordMustBeDeclaredAtNamespaceLevel(keyword),
406
            location: rangeFactory ? util.createLocationFromFileRange(this.event.file, rangeFactory()) : statement.location
14!
407
        });
408
    }
409

410
    private validateFunctionParameterCount(func: FunctionExpression) {
411
        if (func.parameters.length > CallExpression.MaximumArguments) {
2,207✔
412
            //flag every parameter over the limit
413
            for (let i = CallExpression.MaximumArguments; i < func.parameters.length; i++) {
2✔
414
                this.event.program.diagnostics.register({
3✔
415
                    ...DiagnosticMessages.tooManyCallableParameters(func.parameters.length, CallExpression.MaximumArguments),
416
                    location: func.parameters[i]?.tokens.name?.location ?? func.parameters[i]?.location ?? func.location
36!
417
                });
418
            }
419
        }
420
    }
421

422
    private validateEnumDeclaration(stmt: EnumStatement) {
423
        const members = stmt.getMembers();
150✔
424
        //the enum data type is based on the first member value
425
        const enumValueKind = (members.find(x => x.value)?.value as LiteralExpression)?.tokens?.value?.kind ?? TokenKind.IntegerLiteral;
211✔
426
        const memberNames = new Set<string>();
150✔
427
        for (const member of members) {
150✔
428
            const memberNameLower = member.name?.toLowerCase();
297!
429

430
            /**
431
             * flag duplicate member names
432
             */
433
            if (memberNames.has(memberNameLower)) {
297✔
434
                this.event.program.diagnostics.register({
1✔
435
                    ...DiagnosticMessages.duplicateIdentifier(member.name),
436
                    location: member.location
437
                });
438
            } else {
439
                memberNames.add(memberNameLower);
296✔
440
            }
441

442
            //Enforce all member values are the same type
443
            this.validateEnumValueTypes(member, enumValueKind);
297✔
444
        }
445
    }
446

447
    private validateEnumValueTypes(member: EnumMemberStatement, enumValueKind: TokenKind) {
448
        let memberValueKind: TokenKind;
449
        let memberValue: Expression;
450
        if (isUnaryExpression(member.value)) {
297✔
451
            memberValueKind = (member.value?.right as LiteralExpression)?.tokens?.value?.kind;
2!
452
            memberValue = member.value?.right;
2!
453
        } else {
454
            memberValueKind = (member.value as LiteralExpression)?.tokens?.value?.kind;
295✔
455
            memberValue = member.value;
295✔
456
        }
457
        const range = (memberValue ?? member)?.location?.range;
297!
458
        if (
297✔
459
            //is integer enum, has value, that value type is not integer
460
            (enumValueKind === TokenKind.IntegerLiteral && memberValueKind && memberValueKind !== enumValueKind) ||
1,000✔
461
            //has value, that value is not a literal
462
            (memberValue && !isLiteralExpression(memberValue))
463
        ) {
464
            this.event.program.diagnostics.register({
6✔
465
                ...DiagnosticMessages.enumValueMustBeType(
466
                    enumValueKind.replace(/literal$/i, '').toLowerCase()
467
                ),
468
                location: util.createLocationFromFileRange(this.event.file, range)
469
            });
470
        }
471

472
        //is non integer value
473
        if (enumValueKind !== TokenKind.IntegerLiteral) {
297✔
474
            //default value present
475
            if (memberValueKind) {
102✔
476
                //member value is same as enum
477
                if (memberValueKind !== enumValueKind) {
100✔
478
                    this.event.program.diagnostics.register({
1✔
479
                        ...DiagnosticMessages.enumValueMustBeType(
480
                            enumValueKind.replace(/literal$/i, '').toLowerCase()
481
                        ),
482
                        location: util.createLocationFromFileRange(this.event.file, range)
483
                    });
484
                }
485

486
                //default value missing
487
            } else {
488
                this.event.program.diagnostics.register({
2✔
489
                    ...DiagnosticMessages.enumValueIsRequired(
490
                        enumValueKind.replace(/literal$/i, '').toLowerCase()
491
                    ),
492
                    location: util.createLocationFromFileRange(this.event.file, range)
493
                });
494
            }
495
        }
496
    }
497

498

499
    private validateConditionalCompileConst(ccConst: Token) {
500
        const isBool = ccConst.kind === TokenKind.True || ccConst.kind === TokenKind.False;
24✔
501
        if (!isBool && !this.event.file.ast.bsConsts.has(ccConst.text.toLowerCase())) {
24✔
502
            this.event.program.diagnostics.register({
2✔
503
                ...DiagnosticMessages.hashConstDoesNotExist(),
504
                location: ccConst.location
505
            });
506
            return false;
2✔
507
        }
508
        return true;
22✔
509
    }
510

511
    /**
512
     * Find statements defined at the top level (or inside a namespace body) that are not allowed to be there
513
     */
514
    private flagTopLevelStatements() {
515
        const statements = [...this.event.file.ast.statements];
1,929✔
516
        while (statements.length > 0) {
1,929✔
517
            const statement = statements.pop();
3,632✔
518
            if (isNamespaceStatement(statement)) {
3,632✔
519
                statements.push(...statement.body.statements);
596✔
520
            } else {
521
                //only allow these statement types
522
                if (
3,036✔
523
                    !isFunctionStatement(statement) &&
6,617✔
524
                    !isClassStatement(statement) &&
525
                    !isEnumStatement(statement) &&
526
                    !isInterfaceStatement(statement) &&
527
                    !isLibraryStatement(statement) &&
528
                    !isImportStatement(statement) &&
529
                    !isConstStatement(statement) &&
530
                    !isTypecastStatement(statement) &&
531
                    !isConditionalCompileConstStatement(statement) &&
532
                    !isConditionalCompileErrorStatement(statement) &&
533
                    !isConditionalCompileStatement(statement) &&
534
                    !isAliasStatement(statement)
535
                ) {
536
                    this.event.program.diagnostics.register({
8✔
537
                        ...DiagnosticMessages.unexpectedStatementOutsideFunction(),
538
                        location: statement.location
539
                    });
540
                }
541
            }
542
        }
543
    }
544

545
    private getTopOfFileStatements() {
546
        let topOfFileIncludeStatements = [] as Array<LibraryStatement | ImportStatement | TypecastStatement | AliasStatement>;
3,856✔
547
        for (let stmt of this.event.file.parser.ast.statements) {
3,856✔
548
            //if we found a non-library statement, this statement is not at the top of the file
549
            if (isLibraryStatement(stmt) || isImportStatement(stmt) || isTypecastStatement(stmt) || isAliasStatement(stmt)) {
4,172✔
550
                topOfFileIncludeStatements.push(stmt);
474✔
551
            } else {
552
                //break out of the loop, we found all of our library statements
553
                break;
3,698✔
554
            }
555
        }
556
        return topOfFileIncludeStatements;
3,856✔
557
    }
558

559
    private validateTopOfFileStatements() {
560
        let topOfFileStatements = this.getTopOfFileStatements();
1,928✔
561

562
        let statements = [
1,928✔
563
            // eslint-disable-next-line @typescript-eslint/dot-notation
564
            ...this.event.file['_cachedLookups'].libraryStatements,
565
            // eslint-disable-next-line @typescript-eslint/dot-notation
566
            ...this.event.file['_cachedLookups'].importStatements,
567
            // eslint-disable-next-line @typescript-eslint/dot-notation
568
            ...this.event.file['_cachedLookups'].aliasStatements
569
        ];
570
        for (let result of statements) {
1,928✔
571
            //if this statement is not one of the top-of-file statements,
572
            //then add a diagnostic explaining that it is invalid
573
            if (!topOfFileStatements.includes(result)) {
232✔
574
                if (isLibraryStatement(result)) {
5✔
575
                    this.event.program.diagnostics.register({
2✔
576
                        ...DiagnosticMessages.unexpectedStatementLocation('library', 'at the top of the file'),
577
                        location: result.location
578
                    });
579
                } else if (isImportStatement(result)) {
3✔
580
                    this.event.program.diagnostics.register({
1✔
581
                        ...DiagnosticMessages.unexpectedStatementLocation('import', 'at the top of the file'),
582
                        location: result.location
583
                    });
584
                } else if (isAliasStatement(result)) {
2!
585
                    this.event.program.diagnostics.register({
2✔
586
                        ...DiagnosticMessages.unexpectedStatementLocation('alias', 'at the top of the file'),
587
                        location: result.location
588
                    });
589
                }
590
            }
591
        }
592
    }
593

594
    private validateTypecastStatements() {
595
        let topOfFileTypecastStatements = this.getTopOfFileStatements().filter(stmt => isTypecastStatement(stmt));
1,928✔
596

597
        //check only one `typecast` statement at "top" of file (eg. before non import/library statements)
598
        for (let i = 1; i < topOfFileTypecastStatements.length; i++) {
1,928✔
599
            const typecastStmt = topOfFileTypecastStatements[i];
1✔
600
            this.event.program.diagnostics.register({
1✔
601
                ...DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of function or namespace'),
602
                location: typecastStmt.location
603
            });
604
        }
605

606
        // eslint-disable-next-line @typescript-eslint/dot-notation
607
        for (let result of this.event.file['_cachedLookups'].typecastStatements) {
1,928✔
608
            let isBadTypecastObj = false;
22✔
609
            if (!isVariableExpression(result.typecastExpression.obj)) {
22✔
610
                isBadTypecastObj = true;
1✔
611
            } else if (result.typecastExpression.obj.tokens.name.text.toLowerCase() !== 'm') {
21✔
612
                isBadTypecastObj = true;
1✔
613
            }
614
            if (isBadTypecastObj) {
22✔
615
                this.event.program.diagnostics.register({
2✔
616
                    ...DiagnosticMessages.invalidTypecastStatementApplication(util.getAllDottedGetPartsAsString(result.typecastExpression.obj)),
617
                    location: result.typecastExpression.obj.location
618
                });
619
            }
620

621
            if (topOfFileTypecastStatements.includes(result)) {
22✔
622
                // already validated
623
                continue;
10✔
624
            }
625

626
            const block = result.findAncestor<Body | Block>(node => (isBody(node) || isBlock(node)));
12✔
627
            const isFirst = block?.statements[0] === result;
12!
628
            const isAllowedBlock = (isBody(block) || isFunctionExpression(block.parent) || isNamespaceStatement(block.parent));
12!
629

630
            if (!isFirst || !isAllowedBlock) {
12✔
631
                this.event.program.diagnostics.register({
3✔
632
                    ...DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of function or namespace'),
633
                    location: result.location
634
                });
635
            }
636
        }
637
    }
638

639
    private validateContinueStatement(statement: ContinueStatement) {
640
        const validateLoopTypeMatch = (expectedLoopType: TokenKind) => {
8✔
641
            //coerce ForEach to For
642
            expectedLoopType = expectedLoopType === TokenKind.ForEach ? TokenKind.For : expectedLoopType;
7✔
643
            const actualLoopType = statement.tokens.loopType;
7✔
644
            if (actualLoopType && expectedLoopType?.toLowerCase() !== actualLoopType.text?.toLowerCase()) {
7!
645
                this.event.program.diagnostics.register({
3✔
646
                    location: statement.tokens.loopType.location,
647
                    ...DiagnosticMessages.expectedToken(expectedLoopType)
648
                });
649
            }
650
        };
651

652
        //find the parent loop statement
653
        const parent = statement.findAncestor<WhileStatement | ForStatement | ForEachStatement>((node) => {
8✔
654
            if (isWhileStatement(node)) {
18✔
655
                validateLoopTypeMatch(node.tokens.while.kind);
3✔
656
                return true;
3✔
657
            } else if (isForStatement(node)) {
15✔
658
                validateLoopTypeMatch(node.tokens.for.kind);
3✔
659
                return true;
3✔
660
            } else if (isForEachStatement(node)) {
12✔
661
                validateLoopTypeMatch(node.tokens.forEach.kind);
1✔
662
                return true;
1✔
663
            }
664
        });
665
        //flag continue statements found outside of a loop
666
        if (!parent) {
8✔
667
            this.event.program.diagnostics.register({
1✔
668
                location: statement.location,
669
                ...DiagnosticMessages.illegalContinueStatement()
670
            });
671
        }
672
    }
673

674
    /**
675
     * Validate that there are no optional chaining operators on the left-hand-side of an assignment, indexed set, or dotted get
676
     */
677
    private validateNoOptionalChainingInVarSet(parent: AstNode, children: AstNode[]) {
678
        const nodes = [...children, parent];
119✔
679
        //flag optional chaining anywhere in the left of this statement
680
        while (nodes.length > 0) {
119✔
681
            const node = nodes.shift();
238✔
682
            if (
238✔
683
                // a?.b = true or a.b?.c = true
684
                ((isDottedSetStatement(node) || isDottedGetExpression(node)) && node.tokens.dot?.kind === TokenKind.QuestionDot) ||
1,357!
685
                // a.b?[2] = true
686
                (isIndexedGetExpression(node) && (node?.tokens.questionDot?.kind === TokenKind.QuestionDot || node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare)) ||
36!
687
                // a?[1] = true
688
                (isIndexedSetStatement(node) && node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare)
57!
689
            ) {
690
                //try to highlight the entire left-hand-side expression if possible
691
                let range: Range;
692
                if (isDottedSetStatement(parent)) {
8✔
693
                    range = util.createBoundingRange(parent.obj?.location, parent.tokens.dot, parent.tokens.name);
5!
694
                } else if (isIndexedSetStatement(parent)) {
3!
695
                    range = util.createBoundingRange(parent.obj?.location, parent.tokens.openingSquare, ...parent.indexes, parent.tokens.closingSquare);
3!
696
                } else {
UNCOV
697
                    range = node.location?.range;
×
698
                }
699

700
                this.event.program.diagnostics.register({
8✔
701
                    ...DiagnosticMessages.noOptionalChainingInLeftHandSideOfAssignment(),
702
                    location: util.createLocationFromFileRange(this.event.file, range)
703
                });
704
            }
705

706
            if (node === parent) {
238✔
707
                break;
119✔
708
            } else {
709
                nodes.push(node.parent);
119✔
710
            }
711
        }
712
    }
713

714
    private setUpComplementSymbolTables(node: IfStatement | ConditionalCompileStatement, predicate: (node: AstNode) => boolean) {
715
        if (isBlock(node.elseBranch)) {
137✔
716
            const elseTable = node.elseBranch.symbolTable;
26✔
717
            let currentNode = node;
26✔
718
            while (predicate(currentNode)) {
26✔
719
                const thenBranch = (currentNode as IfStatement | ConditionalCompileStatement).thenBranch;
37✔
720
                elseTable.complementOtherTable(thenBranch.symbolTable);
37✔
721
                currentNode = currentNode.parent as IfStatement | ConditionalCompileStatement;
37✔
722
            }
723
        }
724
    }
725
}
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