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

rokucommunity / brighterscript / #12930

13 Aug 2024 05:02PM UTC coverage: 86.193% (-1.7%) from 87.933%
#12930

push

web-flow
Merge 58ad447a2 into 0e968f1c3

10630 of 13125 branches covered (80.99%)

Branch coverage included in aggregate %.

6675 of 7284 new or added lines in 99 files covered. (91.64%)

84 existing lines in 18 files now uncovered.

12312 of 13492 relevant lines covered (91.25%)

26865.48 hits per line

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

88.89
/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, isLibraryStatement, isLiteralExpression, isMethodStatement, isNamespaceStatement, isTypecastStatement, isUnaryExpression, isVariableExpression, 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

19
export class BrsFileValidator {
1✔
20
    constructor(
21
        public event: OnFileValidateEvent<BrsFile>
1,555✔
22
    ) {
23
    }
24

25

26
    public process() {
27
        const unlinkGlobalSymbolTable = this.event.file.parser.symbolTable.pushParentProvider(() => this.event.program.globalScope.symbolTable);
4,644✔
28

29
        util.validateTooDeepFile(this.event.file);
1,555✔
30

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

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

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

47
        this.event.file.ast.bsConsts = bsConstsBackup;
1,555✔
48
        unlinkGlobalSymbolTable();
1,555✔
49
    }
50

51
    /**
52
     * Walk the full AST
53
     */
54
    private walk() {
55

56

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

77
                this.validateEnumDeclaration(node);
132✔
78

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

87
                //register this class
88
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime });
395✔
89
                node.getSymbolTable().addSymbol('m', { definingNode: node, isInstance: true }, nodeType, SymbolTypeFlag.runtime);
395✔
90
                // eslint-disable-next-line no-bitwise
91
                node.parent.getSymbolTable()?.addSymbol(node.tokens.name?.text, { definingNode: node }, nodeType, SymbolTypeFlag.typetime | SymbolTypeFlag.runtime);
395!
92
            },
93
            AssignmentStatement: (node) => {
94
                //register this variable
95
                const nodeType = node.getType({ flags: SymbolTypeFlag.runtime });
595✔
96
                node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true }, nodeType, SymbolTypeFlag.runtime);
595!
97
            },
98
            DottedSetStatement: (node) => {
99
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
80✔
100
            },
101
            IndexedSetStatement: (node) => {
102
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
15✔
103
            },
104
            ForEachStatement: (node) => {
105
                //register the for loop variable
106
                const loopTargetType = node.target.getType({ flags: SymbolTypeFlag.runtime });
22✔
107
                let loopVarType = isArrayType(loopTargetType) ? loopTargetType.defaultType : DynamicType.instance;
22✔
108
                if (!loopTargetType.isResolvable()) {
22✔
109
                    loopVarType = new ArrayDefaultTypeReferenceType(loopTargetType);
1✔
110
                }
111
                node.parent.getSymbolTable()?.addSymbol(node.tokens.item.text, { definingNode: node, isInstance: true }, loopVarType, SymbolTypeFlag.runtime);
22!
112
            },
113
            NamespaceStatement: (node) => {
114
                this.validateDeclarationLocations(node, 'namespace', () => util.createBoundingRange(node.tokens.namespace, node.nameExpression));
543✔
115
                //Namespace Types are added at the Scope level - This is handled when the SymbolTables get linked
116
            },
117
            FunctionStatement: (node) => {
118
                this.validateDeclarationLocations(node, 'function', () => util.createBoundingRange(node.func.tokens.functionType, node.tokens.name));
1,521✔
119
                const funcType = node.getType({ flags: SymbolTypeFlag.typetime });
1,521✔
120

121
                if (node.tokens.name?.text) {
1,521!
122
                    node.parent.getSymbolTable().addSymbol(
1,521✔
123
                        node.tokens.name.text,
124
                        { definingNode: node },
125
                        funcType,
126
                        SymbolTypeFlag.runtime
127
                    );
128
                }
129

130
                const namespace = node.findAncestor(isNamespaceStatement);
1,521✔
131
                //this function is declared inside a namespace
132
                if (namespace) {
1,521✔
133
                    namespace.getSymbolTable().addSymbol(
353✔
134
                        node.tokens.name?.text,
1,059!
135
                        { definingNode: node },
136
                        funcType,
137
                        SymbolTypeFlag.runtime
138
                    );
139
                    //add the transpiled name for namespaced functions to the root symbol table
140
                    const transpiledNamespaceFunctionName = node.getName(ParseMode.BrightScript);
353✔
141

142
                    this.event.file.parser.ast.symbolTable.addSymbol(
353✔
143
                        transpiledNamespaceFunctionName,
144
                        { definingNode: node },
145
                        funcType,
146
                        // eslint-disable-next-line no-bitwise
147
                        SymbolTypeFlag.runtime | SymbolTypeFlag.postTranspile
148
                    );
149
                }
150
            },
151
            FunctionExpression: (node) => {
152
                const funcSymbolTable = node.getSymbolTable();
1,777✔
153
                const isInlineFunc = !(isFunctionStatement(node.parent) || isMethodStatement(node.parent));
1,777✔
154
                if (!funcSymbolTable?.hasSymbol('m', SymbolTypeFlag.runtime) || isInlineFunc) {
1,777!
155
                    if (!isTypecastStatement(node.body?.statements?.[0])) {
26!
156
                        funcSymbolTable?.addSymbol('m', { isInstance: true }, new AssociativeArrayType(), SymbolTypeFlag.runtime);
25!
157
                    }
158
                }
159
                this.validateFunctionParameterCount(node);
1,777✔
160
            },
161
            FunctionParameterExpression: (node) => {
162
                const paramName = node.tokens.name?.text;
953!
163
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime });
953✔
164
                // add param symbol at expression level, so it can be used as default value in other params
165
                const funcExpr = node.findAncestor<FunctionExpression>(isFunctionExpression);
953✔
166
                const funcSymbolTable = funcExpr?.getSymbolTable();
953!
167
                funcSymbolTable?.addSymbol(paramName, { definingNode: node, isInstance: true }, nodeType, SymbolTypeFlag.runtime);
953!
168

169
                //also add param symbol at block level, as it may be redefined, and if so, should show a union
170
                funcExpr.body.getSymbolTable()?.addSymbol(paramName, { definingNode: node, isInstance: true }, nodeType, SymbolTypeFlag.runtime);
953!
171
            },
172
            InterfaceStatement: (node) => {
173
                this.validateDeclarationLocations(node, 'interface', () => util.createBoundingRange(node.tokens.interface, node.tokens.name));
122✔
174

175
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime });
122✔
176
                // eslint-disable-next-line no-bitwise
177
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node }, nodeType, SymbolTypeFlag.typetime);
122✔
178
            },
179
            ConstStatement: (node) => {
180
                this.validateDeclarationLocations(node, 'const', () => util.createBoundingRange(node.tokens.const, node.tokens.name));
135✔
181
                const nodeType = node.getType({ flags: SymbolTypeFlag.runtime });
135✔
182
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true }, nodeType, SymbolTypeFlag.runtime);
135✔
183
            },
184
            CatchStatement: (node) => {
185
                node.parent.getSymbolTable().addSymbol(node.tokens.exceptionVariable.text, { definingNode: node, isInstance: true }, DynamicType.instance, SymbolTypeFlag.runtime);
6✔
186
            },
187
            DimStatement: (node) => {
188
                if (node.tokens.name) {
18!
189
                    node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true }, node.getType({ flags: SymbolTypeFlag.runtime }), SymbolTypeFlag.runtime);
18✔
190
                }
191
            },
192
            ContinueStatement: (node) => {
193
                this.validateContinueStatement(node);
8✔
194
            },
195
            TypecastStatement: (node) => {
196
                node.parent.getSymbolTable().addSymbol('m', { definingNode: node, doNotMerge: true, isInstance: true }, node.getType({ flags: SymbolTypeFlag.typetime }), SymbolTypeFlag.runtime);
19✔
197
            },
198
            ConditionalCompileConstStatement: (node) => {
199
                const assign = node.assignment;
10✔
200
                const constNameLower = assign.tokens.name?.text.toLowerCase();
10!
201
                const astBsConsts = this.event.file.ast.bsConsts;
10✔
202
                if (isLiteralExpression(assign.value)) {
10!
203
                    astBsConsts.set(constNameLower, assign.value.tokens.value.text.toLowerCase() === 'true');
10✔
NEW
204
                } else if (isVariableExpression(assign.value)) {
×
NEW
205
                    if (this.validateConditionalCompileConst(assign.value.tokens.name)) {
×
NEW
206
                        astBsConsts.set(constNameLower, astBsConsts.get(assign.value.tokens.name.text.toLowerCase()));
×
207
                    }
208
                }
209
            },
210
            ConditionalCompileStatement: (node) => {
211
                this.validateConditionalCompileConst(node.tokens.condition);
22✔
212
            },
213
            ConditionalCompileErrorStatement: (node) => {
214
                this.event.program.diagnostics.register({
1✔
215
                    ...DiagnosticMessages.hashError(node.tokens.message.text),
216
                    location: node.location
217
                });
218
            },
219
            AliasStatement: (node) => {
220
                // eslint-disable-next-line no-bitwise
221
                const targetType = node.value.getType({ flags: SymbolTypeFlag.typetime | SymbolTypeFlag.runtime });
30✔
222

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

226
            }
227
        });
228

229
        this.event.file.ast.walk((node, parent) => {
1,555✔
230
            visitor(node, parent);
21,821✔
231
        }, {
232
            walkMode: WalkMode.visitAllRecursive
233
        });
234
    }
235

236
    /**
237
     * Validate that a statement is defined in one of these specific locations
238
     *  - the root of the AST
239
     *  - inside a namespace
240
     * This is applicable to things like FunctionStatement, ClassStatement, NamespaceStatement, EnumStatement, InterfaceStatement
241
     */
242
    private validateDeclarationLocations(statement: Statement, keyword: string, rangeFactory?: () => (Range | undefined)) {
243
        //if nested inside a namespace, or defined at the root of the AST (i.e. in a body that has no parent)
244
        const isOkDeclarationLocation = (parentNode) => {
2,848✔
245
            return isNamespaceStatement(parentNode?.parent) || (isBody(parentNode) && !parentNode?.parent);
2,853!
246
        };
247
        if (isOkDeclarationLocation(statement.parent)) {
2,848✔
248
            return;
2,830✔
249
        }
250

251
        // is this in a top levelconditional compile?
252
        if (isConditionalCompileStatement(statement.parent?.parent)) {
18!
253
            if (isOkDeclarationLocation(statement.parent.parent.parent)) {
5✔
254
                return;
4✔
255
            }
256
        }
257

258
        //the statement was defined in the wrong place. Flag it.
259
        this.event.program.diagnostics.register({
14✔
260
            ...DiagnosticMessages.keywordMustBeDeclaredAtNamespaceLevel(keyword),
261
            location: rangeFactory ? util.createLocationFromFileRange(this.event.file, rangeFactory()) : statement.location
14!
262
        });
263
    }
264

265
    private validateFunctionParameterCount(func: FunctionExpression) {
266
        if (func.parameters.length > CallExpression.MaximumArguments) {
1,777✔
267
            //flag every parameter over the limit
268
            for (let i = CallExpression.MaximumArguments; i < func.parameters.length; i++) {
2✔
269
                this.event.program.diagnostics.register({
3✔
270
                    ...DiagnosticMessages.tooManyCallableParameters(func.parameters.length, CallExpression.MaximumArguments),
271
                    location: func.parameters[i]?.tokens.name?.location ?? func.parameters[i]?.location ?? func.location
36!
272
                });
273
            }
274
        }
275
    }
276

277
    private validateEnumDeclaration(stmt: EnumStatement) {
278
        const members = stmt.getMembers();
132✔
279
        //the enum data type is based on the first member value
280
        const enumValueKind = (members.find(x => x.value)?.value as LiteralExpression)?.tokens?.value?.kind ?? TokenKind.IntegerLiteral;
185✔
281
        const memberNames = new Set<string>();
132✔
282
        for (const member of members) {
132✔
283
            const memberNameLower = member.name?.toLowerCase();
256!
284

285
            /**
286
             * flag duplicate member names
287
             */
288
            if (memberNames.has(memberNameLower)) {
256✔
289
                this.event.program.diagnostics.register({
1✔
290
                    ...DiagnosticMessages.duplicateIdentifier(member.name),
291
                    location: member.location
292
                });
293
            } else {
294
                memberNames.add(memberNameLower);
255✔
295
            }
296

297
            //Enforce all member values are the same type
298
            this.validateEnumValueTypes(member, enumValueKind);
256✔
299
        }
300
    }
301

302
    private validateEnumValueTypes(member: EnumMemberStatement, enumValueKind: TokenKind) {
303
        let memberValueKind: TokenKind;
304
        let memberValue: Expression;
305
        if (isUnaryExpression(member.value)) {
256✔
306
            memberValueKind = (member.value?.right as LiteralExpression)?.tokens?.value?.kind;
2!
307
            memberValue = member.value?.right;
2!
308
        } else {
309
            memberValueKind = (member.value as LiteralExpression)?.tokens?.value?.kind;
254✔
310
            memberValue = member.value;
254✔
311
        }
312
        const range = (memberValue ?? member)?.location?.range;
256!
313
        if (
256✔
314
            //is integer enum, has value, that value type is not integer
315
            (enumValueKind === TokenKind.IntegerLiteral && memberValueKind && memberValueKind !== enumValueKind) ||
853✔
316
            //has value, that value is not a literal
317
            (memberValue && !isLiteralExpression(memberValue))
318
        ) {
319
            this.event.program.diagnostics.register({
6✔
320
                ...DiagnosticMessages.enumValueMustBeType(
321
                    enumValueKind.replace(/literal$/i, '').toLowerCase()
322
                ),
323
                location: util.createLocationFromFileRange(this.event.file, range)
324
            });
325
        }
326

327
        //is non integer value
328
        if (enumValueKind !== TokenKind.IntegerLiteral) {
256✔
329
            //default value present
330
            if (memberValueKind) {
93✔
331
                //member value is same as enum
332
                if (memberValueKind !== enumValueKind) {
91✔
333
                    this.event.program.diagnostics.register({
1✔
334
                        ...DiagnosticMessages.enumValueMustBeType(
335
                            enumValueKind.replace(/literal$/i, '').toLowerCase()
336
                        ),
337
                        location: util.createLocationFromFileRange(this.event.file, range)
338
                    });
339
                }
340

341
                //default value missing
342
            } else {
343
                this.event.program.diagnostics.register({
2✔
344
                    ...DiagnosticMessages.enumValueIsRequired(
345
                        enumValueKind.replace(/literal$/i, '').toLowerCase()
346
                    ),
347
                    location: util.createLocationFromFileRange(this.event.file, range)
348
                });
349
            }
350
        }
351
    }
352

353

354
    private validateConditionalCompileConst(ccConst: Token) {
355
        const isBool = ccConst.kind === TokenKind.True || ccConst.kind === TokenKind.False;
22✔
356
        if (!isBool && !this.event.file.ast.bsConsts.has(ccConst.text.toLowerCase())) {
22✔
357
            this.event.program.diagnostics.register({
2✔
358
                ...DiagnosticMessages.referencedConstDoesNotExist(),
359
                location: ccConst.location
360
            });
361
            return false;
2✔
362
        }
363
        return true;
20✔
364
    }
365

366
    /**
367
     * Find statements defined at the top level (or inside a namespace body) that are not allowed to be there
368
     */
369
    private flagTopLevelStatements() {
370
        const statements = [...this.event.file.ast.statements];
1,555✔
371
        while (statements.length > 0) {
1,555✔
372
            const statement = statements.pop();
3,076✔
373
            if (isNamespaceStatement(statement)) {
3,076✔
374
                statements.push(...statement.body.statements);
538✔
375
            } else {
376
                //only allow these statement types
377
                if (
2,538✔
378
                    !isFunctionStatement(statement) &&
5,844✔
379
                    !isClassStatement(statement) &&
380
                    !isEnumStatement(statement) &&
381
                    !isInterfaceStatement(statement) &&
382
                    !isLibraryStatement(statement) &&
383
                    !isImportStatement(statement) &&
384
                    !isConstStatement(statement) &&
385
                    !isTypecastStatement(statement) &&
386
                    !isConditionalCompileConstStatement(statement) &&
387
                    !isConditionalCompileErrorStatement(statement) &&
388
                    !isConditionalCompileStatement(statement) &&
389
                    !isAliasStatement(statement)
390
                ) {
391
                    this.event.program.diagnostics.register({
7✔
392
                        ...DiagnosticMessages.unexpectedStatementOutsideFunction(),
393
                        location: statement.location
394
                    });
395
                }
396
            }
397
        }
398
    }
399

400
    private getTopOfFileStatements() {
401
        let topOfFileIncludeStatements = [] as Array<LibraryStatement | ImportStatement | TypecastStatement | AliasStatement>;
3,108✔
402
        for (let stmt of this.event.file.parser.ast.statements) {
3,108✔
403
            //if we found a non-library statement, this statement is not at the top of the file
404
            if (isLibraryStatement(stmt) || isImportStatement(stmt) || isTypecastStatement(stmt) || isAliasStatement(stmt)) {
3,430✔
405
                topOfFileIncludeStatements.push(stmt);
452✔
406
            } else {
407
                //break out of the loop, we found all of our library statements
408
                break;
2,978✔
409
            }
410
        }
411
        return topOfFileIncludeStatements;
3,108✔
412
    }
413

414
    private validateTopOfFileStatements() {
415
        let topOfFileStatements = this.getTopOfFileStatements();
1,554✔
416

417
        let statements = [
1,554✔
418
            // eslint-disable-next-line @typescript-eslint/dot-notation
419
            ...this.event.file['_cachedLookups'].libraryStatements,
420
            // eslint-disable-next-line @typescript-eslint/dot-notation
421
            ...this.event.file['_cachedLookups'].importStatements,
422
            // eslint-disable-next-line @typescript-eslint/dot-notation
423
            ...this.event.file['_cachedLookups'].aliasStatements
424
        ];
425
        for (let result of statements) {
1,554✔
426
            //if this statement is not one of the top-of-file statements,
427
            //then add a diagnostic explaining that it is invalid
428
            if (!topOfFileStatements.includes(result)) {
224✔
429
                if (isLibraryStatement(result)) {
5✔
430
                    this.event.program.diagnostics.register({
2✔
431
                        ...DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('library'),
432
                        location: result.location
433
                    });
434
                } else if (isImportStatement(result)) {
3✔
435
                    this.event.program.diagnostics.register({
1✔
436
                        ...DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('import'),
437
                        location: result.location
438
                    });
439
                } else if (isAliasStatement(result)) {
2!
440
                    this.event.program.diagnostics.register({
2✔
441
                        ...DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('alias'),
442
                        location: result.location
443
                    });
444
                }
445
            }
446
        }
447
    }
448

449
    private validateTypecastStatements() {
450
        let topOfFileTypecastStatements = this.getTopOfFileStatements().filter(stmt => isTypecastStatement(stmt));
1,554✔
451

452
        //check only one `typecast` statement at "top" of file (eg. before non import/library statements)
453
        for (let i = 1; i < topOfFileTypecastStatements.length; i++) {
1,554✔
454
            const typecastStmt = topOfFileTypecastStatements[i];
1✔
455
            this.event.program.diagnostics.register({
1✔
456
                ...DiagnosticMessages.typecastStatementMustBeDeclaredAtStart(),
457
                location: typecastStmt.location
458
            });
459
        }
460

461
        // eslint-disable-next-line @typescript-eslint/dot-notation
462
        for (let result of this.event.file['_cachedLookups'].typecastStatements) {
1,554✔
463
            let isBadTypecastObj = false;
19✔
464
            if (!isVariableExpression(result.typecastExpression.obj)) {
19✔
465
                isBadTypecastObj = true;
1✔
466
            } else if (result.typecastExpression.obj.tokens.name.text.toLowerCase() !== 'm') {
18✔
467
                isBadTypecastObj = true;
1✔
468
            }
469
            if (isBadTypecastObj) {
19✔
470
                this.event.program.diagnostics.register({
2✔
471
                    ...DiagnosticMessages.invalidTypecastStatementApplication(util.getAllDottedGetPartsAsString(result.typecastExpression.obj)),
472
                    location: result.typecastExpression.obj.location
473
                });
474
            }
475

476
            if (topOfFileTypecastStatements.includes(result)) {
19✔
477
                // already validated
478
                continue;
7✔
479
            }
480

481
            const block = result.findAncestor<Body | Block>(node => (isBody(node) || isBlock(node)));
12✔
482
            const isFirst = block?.statements[0] === result;
12!
483
            const isAllowedBlock = (isBody(block) || isFunctionExpression(block.parent) || isNamespaceStatement(block.parent));
12!
484

485
            if (!isFirst || !isAllowedBlock) {
12✔
486
                this.event.program.diagnostics.register({
3✔
487
                    ...DiagnosticMessages.typecastStatementMustBeDeclaredAtStart(),
488
                    location: result.location
489
                });
490
            }
491
        }
492
    }
493

494
    private validateContinueStatement(statement: ContinueStatement) {
495
        const validateLoopTypeMatch = (expectedLoopType: TokenKind) => {
8✔
496
            //coerce ForEach to For
497
            expectedLoopType = expectedLoopType === TokenKind.ForEach ? TokenKind.For : expectedLoopType;
7✔
498
            const actualLoopType = statement.tokens.loopType;
7✔
499
            if (actualLoopType && expectedLoopType?.toLowerCase() !== actualLoopType.text?.toLowerCase()) {
7!
500
                this.event.program.diagnostics.register({
3✔
501
                    location: statement.tokens.loopType.location,
502
                    ...DiagnosticMessages.expectedToken(expectedLoopType)
503
                });
504
            }
505
        };
506

507
        //find the parent loop statement
508
        const parent = statement.findAncestor<WhileStatement | ForStatement | ForEachStatement>((node) => {
8✔
509
            if (isWhileStatement(node)) {
18✔
510
                validateLoopTypeMatch(node.tokens.while.kind);
3✔
511
                return true;
3✔
512
            } else if (isForStatement(node)) {
15✔
513
                validateLoopTypeMatch(node.tokens.for.kind);
3✔
514
                return true;
3✔
515
            } else if (isForEachStatement(node)) {
12✔
516
                validateLoopTypeMatch(node.tokens.forEach.kind);
1✔
517
                return true;
1✔
518
            }
519
        });
520
        //flag continue statements found outside of a loop
521
        if (!parent) {
8✔
522
            this.event.program.diagnostics.register({
1✔
523
                location: statement.location,
524
                ...DiagnosticMessages.illegalContinueStatement()
525
            });
526
        }
527
    }
528

529
    /**
530
     * Validate that there are no optional chaining operators on the left-hand-side of an assignment, indexed set, or dotted get
531
     */
532
    private validateNoOptionalChainingInVarSet(parent: AstNode, children: AstNode[]) {
533
        const nodes = [...children, parent];
95✔
534
        //flag optional chaining anywhere in the left of this statement
535
        while (nodes.length > 0) {
95✔
536
            const node = nodes.shift();
190✔
537
            if (
190✔
538
                // a?.b = true or a.b?.c = true
539
                ((isDottedSetStatement(node) || isDottedGetExpression(node)) && node.tokens.dot?.kind === TokenKind.QuestionDot) ||
1,085!
540
                // a.b?[2] = true
541
                (isIndexedGetExpression(node) && (node?.tokens.questionDot?.kind === TokenKind.QuestionDot || node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare)) ||
36!
542
                // a?[1] = true
543
                (isIndexedSetStatement(node) && node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare)
45!
544
            ) {
545
                //try to highlight the entire left-hand-side expression if possible
546
                let range: Range;
547
                if (isDottedSetStatement(parent)) {
8✔
548
                    range = util.createBoundingRange(parent.obj?.location, parent.tokens.dot, parent.tokens.name);
5!
549
                } else if (isIndexedSetStatement(parent)) {
3!
550
                    range = util.createBoundingRange(parent.obj?.location, parent.tokens.openingSquare, ...parent.indexes, parent.tokens.closingSquare);
3!
551
                } else {
NEW
552
                    range = node.location?.range;
×
553
                }
554

555
                this.event.program.diagnostics.register({
8✔
556
                    ...DiagnosticMessages.noOptionalChainingInLeftHandSideOfAssignment(),
557
                    location: util.createLocationFromFileRange(this.event.file, range)
558
                });
559
            }
560

561
            if (node === parent) {
190✔
562
                break;
95✔
563
            } else {
564
                nodes.push(node.parent);
95✔
565
            }
566
        }
567
    }
568
}
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