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

rokucommunity / brighterscript / #12854

25 Jul 2024 05:41PM UTC coverage: 85.626% (-0.6%) from 86.202%
#12854

push

web-flow
Merge 7c29dfd7b into 5f3ffa3fa

10816 of 13510 branches covered (80.06%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 2 files covered. (100.0%)

318 existing lines in 19 files now uncovered.

12279 of 13462 relevant lines covered (91.21%)

26654.75 hits per line

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

85.85
/src/bscPlugin/validation/BrsFileValidator.ts
1
import { isAALiteralExpression, isAliasStatement, isArrayType, isBlock, isBody, isClassStatement, isConditionalCompileConstStatement, isConditionalCompileErrorStatement, isConditionalCompileStatement, isConstStatement, isDottedGetExpression, isDottedSetStatement, isEnumStatement, isForEachStatement, isForStatement, isFunctionExpression, isFunctionStatement, isImportStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceStatement, isLibraryStatement, isLiteralExpression, 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,537✔
22
    ) {
23
    }
24

25

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

29
        util.validateTooDeepFile(this.event.file);
1,537✔
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,537✔
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,537✔
38

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

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

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

56

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

78
                this.validateEnumDeclaration(node);
131✔
79

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

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

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

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

143
                    this.event.file.parser.ast.symbolTable.addSymbol(
350✔
144
                        transpiledNamespaceFunctionName,
145
                        { definingNode: node },
146
                        funcType,
147
                        // eslint-disable-next-line no-bitwise
148
                        SymbolTypeFlag.runtime | SymbolTypeFlag.postTranspile
149
                    );
150
                }
151
            },
152
            FunctionExpression: (node) => {
153
                const funcSymbolTable = node.getSymbolTable();
1,760✔
154
                if (!funcSymbolTable?.hasSymbol('m', SymbolTypeFlag.runtime) || node.findAncestor(isAALiteralExpression)) {
1,760!
155
                    if (!isTypecastStatement(node.body?.statements?.[0])) {
4!
156
                        funcSymbolTable?.addSymbol('m', { isInstance: true }, new AssociativeArrayType(), SymbolTypeFlag.runtime);
3!
157
                    }
158
                }
159
                this.validateFunctionParameterCount(node);
1,760✔
160
            },
161
            FunctionParameterExpression: (node) => {
162
                const paramName = node.tokens.name?.text;
952!
163
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime });
952✔
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);
952✔
166
                const funcSymbolTable = funcExpr?.getSymbolTable();
952!
167
                funcSymbolTable?.addSymbol(paramName, { definingNode: node, isInstance: true }, nodeType, SymbolTypeFlag.runtime);
952!
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);
952!
171
            },
172
            InterfaceStatement: (node) => {
173
                this.validateDeclarationLocations(node, 'interface', () => util.createBoundingRange(node.tokens.interface, node.tokens.name));
121✔
174

175
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime });
121✔
176
                // eslint-disable-next-line no-bitwise
177
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node }, nodeType, SymbolTypeFlag.typetime);
121✔
178
            },
179
            ConstStatement: (node) => {
180
                this.validateDeclarationLocations(node, 'const', () => util.createBoundingRange(node.tokens.const, node.tokens.name));
134✔
181
                const nodeType = node.getType({ flags: SymbolTypeFlag.runtime });
134✔
182
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true }, nodeType, SymbolTypeFlag.runtime);
134✔
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✔
204
                } else if (isVariableExpression(assign.value)) {
×
205
                    if (this.validateConditionalCompileConst(assign.value.tokens.name)) {
×
UNCOV
206
                        astBsConsts.set(constNameLower, astBsConsts.get(assign.value.tokens.name.text.toLowerCase()));
×
207
                    }
208
                }
209
            },
210
            ConditionalCompileStatement: (node) => {
211
                this.validateConditionalCompileConst(node.tokens.condition);
17✔
212
            },
213
            ConditionalCompileErrorStatement: (node) => {
214
                this.event.program.diagnostics.register({
1✔
215
                    file: this.event.file,
216
                    ...DiagnosticMessages.hashError(node.tokens.message.text),
217
                    range: node.location?.range
3!
218
                });
219
            },
220
            AliasStatement: (node) => {
221
                // eslint-disable-next-line no-bitwise
222
                const targetType = node.value.getType({ flags: SymbolTypeFlag.typetime | SymbolTypeFlag.runtime });
30✔
223

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

227
            }
228
        });
229

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

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

252
        // is this in a top levelconditional compile?
253
        if (isConditionalCompileStatement(statement.parent?.parent)) {
15!
254
            if (isOkDeclarationLocation(statement.parent.parent.parent)) {
2✔
255
                return;
1✔
256
            }
257
        }
258

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

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

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

288
            /**
289
             * flag duplicate member names
290
             */
291
            if (memberNames.has(memberNameLower)) {
255✔
292
                this.event.program.diagnostics.register({
1✔
293
                    file: this.event.file,
294
                    ...DiagnosticMessages.duplicateIdentifier(member.name),
295
                    range: member.location?.range
3!
296
                });
297
            } else {
298
                memberNames.add(memberNameLower);
254✔
299
            }
300

301
            //Enforce all member values are the same type
302
            this.validateEnumValueTypes(member, enumValueKind);
255✔
303
        }
304
    }
305

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

332
        //is non integer value
333
        if (enumValueKind !== TokenKind.IntegerLiteral) {
255✔
334
            //default value present
335
            if (memberValueKind) {
92✔
336
                //member value is same as enum
337
                if (memberValueKind !== enumValueKind) {
90✔
338
                    this.event.program.diagnostics.register({
1✔
339
                        file: this.event.file,
340
                        ...DiagnosticMessages.enumValueMustBeType(
341
                            enumValueKind.replace(/literal$/i, '').toLowerCase()
342
                        ),
343
                        range: range
344
                    });
345
                }
346

347
                //default value missing
348
            } else {
349
                this.event.program.diagnostics.register({
2✔
350
                    file: this.event.file,
351
                    ...DiagnosticMessages.enumValueIsRequired(
352
                        enumValueKind.replace(/literal$/i, '').toLowerCase()
353
                    ),
354
                    range: range
355
                });
356
            }
357
        }
358
    }
359

360

361
    private validateConditionalCompileConst(ccConst: Token) {
362
        const isBool = ccConst.kind === TokenKind.True || ccConst.kind === TokenKind.False;
17✔
363
        if (!isBool && !this.event.file.ast.bsConsts.has(ccConst.text.toLowerCase())) {
17✔
364
            this.event.program.diagnostics.register({
2✔
365
                file: this.event.file,
366
                ...DiagnosticMessages.referencedConstDoesNotExist(),
367
                range: ccConst.location?.range
6!
368
            });
369
            return false;
2✔
370
        }
371
        return true;
15✔
372
    }
373

374
    /**
375
     * Find statements defined at the top level (or inside a namespace body) that are not allowed to be there
376
     */
377
    private flagTopLevelStatements() {
378
        const statements = [...this.event.file.ast.statements];
1,537✔
379
        while (statements.length > 0) {
1,537✔
380
            const statement = statements.pop();
3,051✔
381
            if (isNamespaceStatement(statement)) {
3,051✔
382
                statements.push(...statement.body.statements);
533✔
383
            } else {
384
                //only allow these statement types
385
                if (
2,518✔
386
                    !isFunctionStatement(statement) &&
5,772✔
387
                    !isClassStatement(statement) &&
388
                    !isEnumStatement(statement) &&
389
                    !isInterfaceStatement(statement) &&
390
                    !isLibraryStatement(statement) &&
391
                    !isImportStatement(statement) &&
392
                    !isConstStatement(statement) &&
393
                    !isTypecastStatement(statement) &&
394
                    !isConditionalCompileConstStatement(statement) &&
395
                    !isConditionalCompileErrorStatement(statement) &&
396
                    !isConditionalCompileStatement(statement) &&
397
                    !isAliasStatement(statement)
398
                ) {
399
                    this.event.program.diagnostics.register({
7✔
400
                        file: this.event.file,
401
                        ...DiagnosticMessages.unexpectedStatementOutsideFunction(),
402
                        range: statement.location?.range
21!
403
                    });
404
                }
405
            }
406
        }
407
    }
408

409
    private getTopOfFileStatements() {
410
        let topOfFileIncludeStatements = [] as Array<LibraryStatement | ImportStatement | TypecastStatement | AliasStatement>;
3,072✔
411
        for (let stmt of this.event.file.parser.ast.statements) {
3,072✔
412
            //if we found a non-library statement, this statement is not at the top of the file
413
            if (isLibraryStatement(stmt) || isImportStatement(stmt) || isTypecastStatement(stmt) || isAliasStatement(stmt)) {
3,394✔
414
                topOfFileIncludeStatements.push(stmt);
448✔
415
            } else {
416
                //break out of the loop, we found all of our library statements
417
                break;
2,946✔
418
            }
419
        }
420
        return topOfFileIncludeStatements;
3,072✔
421
    }
422

423
    private validateTopOfFileStatements() {
424
        let topOfFileStatements = this.getTopOfFileStatements();
1,536✔
425

426
        let statements = [
1,536✔
427
            // eslint-disable-next-line @typescript-eslint/dot-notation
428
            ...this.event.file['_cachedLookups'].libraryStatements,
429
            // eslint-disable-next-line @typescript-eslint/dot-notation
430
            ...this.event.file['_cachedLookups'].importStatements,
431
            // eslint-disable-next-line @typescript-eslint/dot-notation
432
            ...this.event.file['_cachedLookups'].aliasStatements
433
        ];
434
        for (let result of statements) {
1,536✔
435
            //if this statement is not one of the top-of-file statements,
436
            //then add a diagnostic explaining that it is invalid
437
            if (!topOfFileStatements.includes(result)) {
222✔
438
                if (isLibraryStatement(result)) {
5✔
439
                    this.event.program.diagnostics.register({
2✔
440
                        ...DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('library'),
441
                        range: result.location?.range,
6!
442
                        file: this.event.file
443
                    });
444
                } else if (isImportStatement(result)) {
3✔
445
                    this.event.program.diagnostics.register({
1✔
446
                        ...DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('import'),
447
                        range: result.location?.range,
3!
448
                        file: this.event.file
449
                    });
450
                } else if (isAliasStatement(result)) {
2!
451
                    this.event.program.diagnostics.register({
2✔
452
                        ...DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('alias'),
453
                        range: result.location?.range,
6!
454
                        file: this.event.file
455
                    });
456
                }
457
            }
458
        }
459
    }
460

461
    private validateTypecastStatements() {
462
        let topOfFileTypecastStatements = this.getTopOfFileStatements().filter(stmt => isTypecastStatement(stmt));
1,536✔
463

464
        //check only one `typecast` statement at "top" of file (eg. before non import/library statements)
465
        for (let i = 1; i < topOfFileTypecastStatements.length; i++) {
1,536✔
466
            const typecastStmt = topOfFileTypecastStatements[i];
1✔
467
            this.event.program.diagnostics.register({
1✔
468
                ...DiagnosticMessages.typecastStatementMustBeDeclaredAtStart(),
469
                range: typecastStmt.location?.range,
3!
470
                file: this.event.file
471
            });
472
        }
473

474
        // eslint-disable-next-line @typescript-eslint/dot-notation
475
        for (let result of this.event.file['_cachedLookups'].typecastStatements) {
1,536✔
476
            let isBadTypecastObj = false;
19✔
477
            if (!isVariableExpression(result.typecastExpression.obj)) {
19✔
478
                isBadTypecastObj = true;
1✔
479
            } else if (result.typecastExpression.obj.tokens.name.text.toLowerCase() !== 'm') {
18✔
480
                isBadTypecastObj = true;
1✔
481
            }
482
            if (isBadTypecastObj) {
19✔
483
                this.event.program.diagnostics.register({
2✔
484
                    ...DiagnosticMessages.invalidTypecastStatementApplication(util.getAllDottedGetPartsAsString(result.typecastExpression.obj)),
485
                    range: result.typecastExpression.obj.location?.range,
6!
486
                    file: this.event.file
487
                });
488
            }
489

490
            if (topOfFileTypecastStatements.includes(result)) {
19✔
491
                // already validated
492
                continue;
7✔
493
            }
494

495
            const block = result.findAncestor<Body | Block>(node => (isBody(node) || isBlock(node)));
12✔
496
            const isFirst = block?.statements[0] === result;
12!
497
            const isAllowedBlock = (isBody(block) || isFunctionExpression(block.parent) || isNamespaceStatement(block.parent));
12!
498

499
            if (!isFirst || !isAllowedBlock) {
12✔
500
                this.event.program.diagnostics.register({
3✔
501
                    ...DiagnosticMessages.typecastStatementMustBeDeclaredAtStart(),
502
                    range: result.location?.range,
9!
503
                    file: this.event.file
504
                });
505
            }
506
        }
507
    }
508

509
    private validateContinueStatement(statement: ContinueStatement) {
510
        const validateLoopTypeMatch = (expectedLoopType: TokenKind) => {
8✔
511
            //coerce ForEach to For
512
            expectedLoopType = expectedLoopType === TokenKind.ForEach ? TokenKind.For : expectedLoopType;
7✔
513
            const actualLoopType = statement.tokens.loopType;
7✔
514
            if (actualLoopType && expectedLoopType?.toLowerCase() !== actualLoopType.text?.toLowerCase()) {
7!
515
                this.event.program.diagnostics.register({
3✔
516
                    file: this.event.file,
517
                    range: statement.tokens.loopType.location?.range,
9!
518
                    ...DiagnosticMessages.expectedToken(expectedLoopType)
519
                });
520
            }
521
        };
522

523
        //find the parent loop statement
524
        const parent = statement.findAncestor<WhileStatement | ForStatement | ForEachStatement>((node) => {
8✔
525
            if (isWhileStatement(node)) {
18✔
526
                validateLoopTypeMatch(node.tokens.while.kind);
3✔
527
                return true;
3✔
528
            } else if (isForStatement(node)) {
15✔
529
                validateLoopTypeMatch(node.tokens.for.kind);
3✔
530
                return true;
3✔
531
            } else if (isForEachStatement(node)) {
12✔
532
                validateLoopTypeMatch(node.tokens.forEach.kind);
1✔
533
                return true;
1✔
534
            }
535
        });
536
        //flag continue statements found outside of a loop
537
        if (!parent) {
8✔
538
            this.event.program.diagnostics.register({
1✔
539
                file: this.event.file,
540
                range: statement.location?.range,
3!
541
                ...DiagnosticMessages.illegalContinueStatement()
542
            });
543
        }
544
    }
545

546
    /**
547
     * Validate that there are no optional chaining operators on the left-hand-side of an assignment, indexed set, or dotted get
548
     */
549
    private validateNoOptionalChainingInVarSet(parent: AstNode, children: AstNode[]) {
550
        const nodes = [...children, parent];
91✔
551
        //flag optional chaining anywhere in the left of this statement
552
        while (nodes.length > 0) {
91✔
553
            const node = nodes.shift();
182✔
554
            if (
182✔
555
                // a?.b = true or a.b?.c = true
556
                ((isDottedSetStatement(node) || isDottedGetExpression(node)) && node.tokens.dot?.kind === TokenKind.QuestionDot) ||
1,041!
557
                // a.b?[2] = true
558
                (isIndexedGetExpression(node) && (node?.tokens.questionDot?.kind === TokenKind.QuestionDot || node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare)) ||
36!
559
                // a?[1] = true
560
                (isIndexedSetStatement(node) && node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare)
45!
561
            ) {
562
                //try to highlight the entire left-hand-side expression if possible
563
                let range: Range;
564
                if (isDottedSetStatement(parent)) {
8✔
565
                    range = util.createBoundingRange(parent.obj?.location, parent.tokens.dot, parent.tokens.name);
5!
566
                } else if (isIndexedSetStatement(parent)) {
3!
567
                    range = util.createBoundingRange(parent.obj?.location, parent.tokens.openingSquare, ...parent.indexes, parent.tokens.closingSquare);
3!
568
                } else {
UNCOV
569
                    range = node.location?.range;
×
570
                }
571

572
                this.event.program.diagnostics.register({
8✔
573
                    file: this.event.file,
574
                    ...DiagnosticMessages.noOptionalChainingInLeftHandSideOfAssignment(),
575
                    range: range
576
                });
577
            }
578

579
            if (node === parent) {
182✔
580
                break;
91✔
581
            } else {
582
                nodes.push(node.parent);
91✔
583
            }
584
        }
585
    }
586
}
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