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

rokucommunity / brighterscript / #13605

13 Jan 2025 08:51PM UTC coverage: 86.913% (+0.01%) from 86.902%
#13605

push

web-flow
Merge 8c5f8741e into 9d6ef67ba

11976 of 14547 branches covered (82.33%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

103 existing lines in 10 files now uncovered.

12982 of 14169 relevant lines covered (91.62%)

31777.91 hits per line

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

86.66
/src/bscPlugin/validation/BrsFileValidator.ts
1
import { isAliasStatement, isArrayType, isBlock, isBody, isClassStatement, isConditionalCompileConstStatement, isConditionalCompileErrorStatement, isConditionalCompileStatement, isConstStatement, isDottedGetExpression, isDottedSetStatement, isEnumStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, 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 } 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,684✔
24
    ) {
25
    }
26

27

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

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

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

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

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

59
        const visitor = createVisitor({
1,684✔
60
            MethodStatement: (node) => {
61
                //add the `super` symbol to class methods
62
                if (isClassStatement(node.parent) && node.parent.hasParentClass()) {
232✔
63
                    const data: ExtraSymbolData = {};
70✔
64
                    const parentClassType = node.parent.parentClassName.getType({ flags: SymbolTypeFlag.typetime, data: data });
70✔
65
                    node.func.body.getSymbolTable().addSymbol('super', { ...data, isInstance: true }, parentClassType, SymbolTypeFlag.runtime);
70✔
66
                }
67
            },
68
            CallfuncExpression: (node) => {
69
                if (node.args.length > 5) {
19✔
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));
141✔
78

79
                this.validateEnumDeclaration(node);
141✔
80

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

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

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

99
                    this.event.file.parser.ast.symbolTable.addSymbol(
128✔
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 = {};
663✔
110
                //register this variable
111
                let nodeType = node.getType({ flags: SymbolTypeFlag.runtime, data: data });
663✔
112
                if (isInvalidType(nodeType) || isVoidType(nodeType)) {
663✔
113
                    nodeType = DynamicType.instance;
12✔
114
                }
115
                node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true, isFromDocComment: data.isFromDocComment }, nodeType, SymbolTypeFlag.runtime);
663!
116
            },
117
            DottedSetStatement: (node) => {
118
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
93✔
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 });
24✔
126
                let loopVarType = isArrayType(loopTargetType) ? loopTargetType.defaultType : DynamicType.instance;
24✔
127
                if (!loopTargetType.isResolvable()) {
24✔
128
                    loopVarType = new ArrayDefaultTypeReferenceType(loopTargetType);
1✔
129
                }
130
                node.parent.getSymbolTable()?.addSymbol(node.tokens.item.text, { definingNode: node, isInstance: true }, loopVarType, SymbolTypeFlag.runtime);
24!
131
            },
132
            NamespaceStatement: (node) => {
133
                this.validateDeclarationLocations(node, 'namespace', () => util.createBoundingRange(node.tokens.namespace, node.nameExpression));
589✔
134
                //Namespace Types are added at the Scope level - This is handled when the SymbolTables get linked
135
            },
136
            FunctionStatement: (node) => {
137
                this.validateDeclarationLocations(node, 'function', () => util.createBoundingRange(node.func.tokens.functionType, node.tokens.name));
1,688✔
138
                const funcType = node.getType({ flags: SymbolTypeFlag.typetime });
1,688✔
139

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

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

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

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

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

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

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

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

278
            },
279
            AstNode: (node) => {
280
                //check for doc comments
281
                if (!node.leadingTrivia || node.leadingTrivia.length === 0) {
23,815✔
282
                    return;
4,179✔
283
                }
284
                const doc = brsDocParser.parseNode(node);
19,636✔
285
                if (doc.tags.length === 0) {
19,636✔
286
                    return;
19,590✔
287
                }
288

289
                let funcExpr = node.findAncestor<FunctionExpression>(isFunctionExpression);
46✔
290
                if (funcExpr) {
46✔
291
                    // handle comment tags inside a function expression
292
                    this.processDocTagsInFunction(doc, node, funcExpr);
8✔
293
                } else {
294
                    //handle comment tags outside of a function expression
295
                    this.processDocTagsAtTopLevel(doc, node);
38✔
296
                }
297
            }
298
        });
299

300
        this.event.file.ast.walk((node, parent) => {
1,684✔
301
            visitor(node, parent);
23,815✔
302
        }, {
303
            walkMode: WalkMode.visitAllRecursive
304
        });
305
    }
306

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

310
        // For example, declaring variable types:
311
        // const symbolTable = funcExpr.body.getSymbolTable();
312

313
        // for (const varTag of doc.getAllTags(BrsDocTagKind.Var)) {
314
        //     const varName = (varTag as BrsDocParamTag).name;
315
        //     const varTypeStr = (varTag as BrsDocParamTag).type;
316
        //     const data: ExtraSymbolData = {};
317
        //     const type = doc.getTypeFromContext(varTypeStr, node, { flags: SymbolTypeFlag.typetime, fullName: varTypeStr, data: data, tableProvider: () => symbolTable });
318
        //     if (type) {
319
        //         symbolTable.addSymbol(varName, { ...data, isFromDocComment: true }, type, SymbolTypeFlag.runtime);
320
        //     }
321
        // }
322
    }
323

324
    private processDocTagsAtTopLevel(doc: BrightScriptDoc, node: AstNode) {
325
        //TODO:
326
        // - handle import statements?
327
        // - handle library statements?
328
        // - handle typecast statements?
329
        // - handle alias statements?
330
        // - handle const statements?
331
        // - allow interface definitions?
332
    }
333

334
    /**
335
     * Validate that a statement is defined in one of these specific locations
336
     *  - the root of the AST
337
     *  - inside a namespace
338
     * This is applicable to things like FunctionStatement, ClassStatement, NamespaceStatement, EnumStatement, InterfaceStatement
339
     */
340
    private validateDeclarationLocations(statement: Statement, keyword: string, rangeFactory?: () => (Range | undefined)) {
341
        //if nested inside a namespace, or defined at the root of the AST (i.e. in a body that has no parent)
342
        const isOkDeclarationLocation = (parentNode) => {
3,096✔
343
            return isNamespaceStatement(parentNode?.parent) || (isBody(parentNode) && !parentNode?.parent);
3,101!
344
        };
345
        if (isOkDeclarationLocation(statement.parent)) {
3,096✔
346
            return;
3,078✔
347
        }
348

349
        // is this in a top levelconditional compile?
350
        if (isConditionalCompileStatement(statement.parent?.parent)) {
18!
351
            if (isOkDeclarationLocation(statement.parent.parent.parent)) {
5✔
352
                return;
4✔
353
            }
354
        }
355

356
        //the statement was defined in the wrong place. Flag it.
357
        this.event.program.diagnostics.register({
14✔
358
            ...DiagnosticMessages.keywordMustBeDeclaredAtNamespaceLevel(keyword),
359
            location: rangeFactory ? util.createLocationFromFileRange(this.event.file, rangeFactory()) : statement.location
14!
360
        });
361
    }
362

363
    private validateFunctionParameterCount(func: FunctionExpression) {
364
        if (func.parameters.length > CallExpression.MaximumArguments) {
1,951✔
365
            //flag every parameter over the limit
366
            for (let i = CallExpression.MaximumArguments; i < func.parameters.length; i++) {
2✔
367
                this.event.program.diagnostics.register({
3✔
368
                    ...DiagnosticMessages.tooManyCallableParameters(func.parameters.length, CallExpression.MaximumArguments),
369
                    location: func.parameters[i]?.tokens.name?.location ?? func.parameters[i]?.location ?? func.location
36!
370
                });
371
            }
372
        }
373
    }
374

375
    private validateEnumDeclaration(stmt: EnumStatement) {
376
        const members = stmt.getMembers();
141✔
377
        //the enum data type is based on the first member value
378
        const enumValueKind = (members.find(x => x.value)?.value as LiteralExpression)?.tokens?.value?.kind ?? TokenKind.IntegerLiteral;
198✔
379
        const memberNames = new Set<string>();
141✔
380
        for (const member of members) {
141✔
381
            const memberNameLower = member.name?.toLowerCase();
284!
382

383
            /**
384
             * flag duplicate member names
385
             */
386
            if (memberNames.has(memberNameLower)) {
284✔
387
                this.event.program.diagnostics.register({
1✔
388
                    ...DiagnosticMessages.duplicateIdentifier(member.name),
389
                    location: member.location
390
                });
391
            } else {
392
                memberNames.add(memberNameLower);
283✔
393
            }
394

395
            //Enforce all member values are the same type
396
            this.validateEnumValueTypes(member, enumValueKind);
284✔
397
        }
398
    }
399

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

425
        //is non integer value
426
        if (enumValueKind !== TokenKind.IntegerLiteral) {
284✔
427
            //default value present
428
            if (memberValueKind) {
101✔
429
                //member value is same as enum
430
                if (memberValueKind !== enumValueKind) {
99✔
431
                    this.event.program.diagnostics.register({
1✔
432
                        ...DiagnosticMessages.enumValueMustBeType(
433
                            enumValueKind.replace(/literal$/i, '').toLowerCase()
434
                        ),
435
                        location: util.createLocationFromFileRange(this.event.file, range)
436
                    });
437
                }
438

439
                //default value missing
440
            } else {
441
                this.event.program.diagnostics.register({
2✔
442
                    ...DiagnosticMessages.enumValueIsRequired(
443
                        enumValueKind.replace(/literal$/i, '').toLowerCase()
444
                    ),
445
                    location: util.createLocationFromFileRange(this.event.file, range)
446
                });
447
            }
448
        }
449
    }
450

451

452
    private validateConditionalCompileConst(ccConst: Token) {
453
        const isBool = ccConst.kind === TokenKind.True || ccConst.kind === TokenKind.False;
22✔
454
        if (!isBool && !this.event.file.ast.bsConsts.has(ccConst.text.toLowerCase())) {
22✔
455
            this.event.program.diagnostics.register({
2✔
456
                ...DiagnosticMessages.hashConstDoesNotExist(),
457
                location: ccConst.location
458
            });
459
            return false;
2✔
460
        }
461
        return true;
20✔
462
    }
463

464
    /**
465
     * Find statements defined at the top level (or inside a namespace body) that are not allowed to be there
466
     */
467
    private flagTopLevelStatements() {
468
        const statements = [...this.event.file.ast.statements];
1,684✔
469
        while (statements.length > 0) {
1,684✔
470
            const statement = statements.pop();
3,325✔
471
            if (isNamespaceStatement(statement)) {
3,325✔
472
                statements.push(...statement.body.statements);
584✔
473
            } else {
474
                //only allow these statement types
475
                if (
2,741✔
476
                    !isFunctionStatement(statement) &&
6,153✔
477
                    !isClassStatement(statement) &&
478
                    !isEnumStatement(statement) &&
479
                    !isInterfaceStatement(statement) &&
480
                    !isLibraryStatement(statement) &&
481
                    !isImportStatement(statement) &&
482
                    !isConstStatement(statement) &&
483
                    !isTypecastStatement(statement) &&
484
                    !isConditionalCompileConstStatement(statement) &&
485
                    !isConditionalCompileErrorStatement(statement) &&
486
                    !isConditionalCompileStatement(statement) &&
487
                    !isAliasStatement(statement)
488
                ) {
489
                    this.event.program.diagnostics.register({
8✔
490
                        ...DiagnosticMessages.unexpectedStatementOutsideFunction(),
491
                        location: statement.location
492
                    });
493
                }
494
            }
495
        }
496
    }
497

498
    private getTopOfFileStatements() {
499
        let topOfFileIncludeStatements = [] as Array<LibraryStatement | ImportStatement | TypecastStatement | AliasStatement>;
3,366✔
500
        for (let stmt of this.event.file.parser.ast.statements) {
3,366✔
501
            //if we found a non-library statement, this statement is not at the top of the file
502
            if (isLibraryStatement(stmt) || isImportStatement(stmt) || isTypecastStatement(stmt) || isAliasStatement(stmt)) {
3,688✔
503
                topOfFileIncludeStatements.push(stmt);
452✔
504
            } else {
505
                //break out of the loop, we found all of our library statements
506
                break;
3,236✔
507
            }
508
        }
509
        return topOfFileIncludeStatements;
3,366✔
510
    }
511

512
    private validateTopOfFileStatements() {
513
        let topOfFileStatements = this.getTopOfFileStatements();
1,683✔
514

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

547
    private validateTypecastStatements() {
548
        let topOfFileTypecastStatements = this.getTopOfFileStatements().filter(stmt => isTypecastStatement(stmt));
1,683✔
549

550
        //check only one `typecast` statement at "top" of file (eg. before non import/library statements)
551
        for (let i = 1; i < topOfFileTypecastStatements.length; i++) {
1,683✔
552
            const typecastStmt = topOfFileTypecastStatements[i];
1✔
553
            this.event.program.diagnostics.register({
1✔
554
                ...DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of function or namespace'),
555
                location: typecastStmt.location
556
            });
557
        }
558

559
        // eslint-disable-next-line @typescript-eslint/dot-notation
560
        for (let result of this.event.file['_cachedLookups'].typecastStatements) {
1,683✔
561
            let isBadTypecastObj = false;
19✔
562
            if (!isVariableExpression(result.typecastExpression.obj)) {
19✔
563
                isBadTypecastObj = true;
1✔
564
            } else if (result.typecastExpression.obj.tokens.name.text.toLowerCase() !== 'm') {
18✔
565
                isBadTypecastObj = true;
1✔
566
            }
567
            if (isBadTypecastObj) {
19✔
568
                this.event.program.diagnostics.register({
2✔
569
                    ...DiagnosticMessages.invalidTypecastStatementApplication(util.getAllDottedGetPartsAsString(result.typecastExpression.obj)),
570
                    location: result.typecastExpression.obj.location
571
                });
572
            }
573

574
            if (topOfFileTypecastStatements.includes(result)) {
19✔
575
                // already validated
576
                continue;
7✔
577
            }
578

579
            const block = result.findAncestor<Body | Block>(node => (isBody(node) || isBlock(node)));
12✔
580
            const isFirst = block?.statements[0] === result;
12!
581
            const isAllowedBlock = (isBody(block) || isFunctionExpression(block.parent) || isNamespaceStatement(block.parent));
12!
582

583
            if (!isFirst || !isAllowedBlock) {
12✔
584
                this.event.program.diagnostics.register({
3✔
585
                    ...DiagnosticMessages.unexpectedStatementLocation('typecast', 'at the top of the file or beginning of function or namespace'),
586
                    location: result.location
587
                });
588
            }
589
        }
590
    }
591

592
    private validateContinueStatement(statement: ContinueStatement) {
593
        const validateLoopTypeMatch = (expectedLoopType: TokenKind) => {
8✔
594
            //coerce ForEach to For
595
            expectedLoopType = expectedLoopType === TokenKind.ForEach ? TokenKind.For : expectedLoopType;
7✔
596
            const actualLoopType = statement.tokens.loopType;
7✔
597
            if (actualLoopType && expectedLoopType?.toLowerCase() !== actualLoopType.text?.toLowerCase()) {
7!
598
                this.event.program.diagnostics.register({
3✔
599
                    location: statement.tokens.loopType.location,
600
                    ...DiagnosticMessages.expectedToken(expectedLoopType)
601
                });
602
            }
603
        };
604

605
        //find the parent loop statement
606
        const parent = statement.findAncestor<WhileStatement | ForStatement | ForEachStatement>((node) => {
8✔
607
            if (isWhileStatement(node)) {
18✔
608
                validateLoopTypeMatch(node.tokens.while.kind);
3✔
609
                return true;
3✔
610
            } else if (isForStatement(node)) {
15✔
611
                validateLoopTypeMatch(node.tokens.for.kind);
3✔
612
                return true;
3✔
613
            } else if (isForEachStatement(node)) {
12✔
614
                validateLoopTypeMatch(node.tokens.forEach.kind);
1✔
615
                return true;
1✔
616
            }
617
        });
618
        //flag continue statements found outside of a loop
619
        if (!parent) {
8✔
620
            this.event.program.diagnostics.register({
1✔
621
                location: statement.location,
622
                ...DiagnosticMessages.illegalContinueStatement()
623
            });
624
        }
625
    }
626

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

653
                this.event.program.diagnostics.register({
8✔
654
                    ...DiagnosticMessages.noOptionalChainingInLeftHandSideOfAssignment(),
655
                    location: util.createLocationFromFileRange(this.event.file, range)
656
                });
657
            }
658

659
            if (node === parent) {
224✔
660
                break;
112✔
661
            } else {
662
                nodes.push(node.parent);
112✔
663
            }
664
        }
665
    }
666
}
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