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

rokucommunity / brighterscript / #13031

05 Sep 2024 06:52PM UTC coverage: 86.418%. Remained the same
#13031

push

web-flow
Merge 0fda0c105 into 43cbf8b72

10784 of 13272 branches covered (81.25%)

Branch coverage included in aggregate %.

153 of 158 new or added lines in 7 files covered. (96.84%)

157 existing lines in 6 files now uncovered.

12484 of 13653 relevant lines covered (91.44%)

27481.68 hits per line

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

89.03
/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,595✔
22
    ) {
23
    }
24

25

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

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

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

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

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

56

57
        const visitor = createVisitor({
1,595✔
58
            MethodStatement: (node) => {
59
                //add the `super` symbol to class methods
60
                if (isClassStatement(node.parent) && node.parent.hasParentClass()) {
232✔
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));
133✔
76

77
                this.validateEnumDeclaration(node);
133✔
78

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

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

93
                if (node.findAncestor(isNamespaceStatement)) {
402✔
94
                    //add the transpiled name for namespaced constructors to the root symbol table
95
                    const transpiledClassConstructor = node.getName(ParseMode.BrightScript);
126✔
96

97
                    this.event.file.parser.ast.symbolTable.addSymbol(
126✔
98
                        transpiledClassConstructor,
99
                        { definingNode: node },
100
                        node.getConstructorType(),
101
                        // eslint-disable-next-line no-bitwise
102
                        SymbolTypeFlag.runtime | SymbolTypeFlag.postTranspile
103
                    );
104
                }
105
            },
106
            AssignmentStatement: (node) => {
107
                const data: ExtraSymbolData = {};
608✔
108
                //register this variable
109
                const nodeType = node.getType({ flags: SymbolTypeFlag.runtime, data: data });
608✔
110
                node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true, isFromDocComment: data.isFromDocComment }, nodeType, SymbolTypeFlag.runtime);
608!
111
            },
112
            DottedSetStatement: (node) => {
113
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
85✔
114
            },
115
            IndexedSetStatement: (node) => {
116
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
15✔
117
            },
118
            ForEachStatement: (node) => {
119
                //register the for loop variable
120
                const loopTargetType = node.target.getType({ flags: SymbolTypeFlag.runtime });
24✔
121
                let loopVarType = isArrayType(loopTargetType) ? loopTargetType.defaultType : DynamicType.instance;
24✔
122
                if (!loopTargetType.isResolvable()) {
24✔
123
                    loopVarType = new ArrayDefaultTypeReferenceType(loopTargetType);
1✔
124
                }
125
                node.parent.getSymbolTable()?.addSymbol(node.tokens.item.text, { definingNode: node, isInstance: true }, loopVarType, SymbolTypeFlag.runtime);
24!
126
            },
127
            NamespaceStatement: (node) => {
128
                this.validateDeclarationLocations(node, 'namespace', () => util.createBoundingRange(node.tokens.namespace, node.nameExpression));
563✔
129
                //Namespace Types are added at the Scope level - This is handled when the SymbolTables get linked
130
            },
131
            FunctionStatement: (node) => {
132
                this.validateDeclarationLocations(node, 'function', () => util.createBoundingRange(node.func.tokens.functionType, node.tokens.name));
1,563✔
133
                const funcType = node.getType({ flags: SymbolTypeFlag.typetime });
1,563✔
134

135
                if (node.tokens.name?.text) {
1,563!
136
                    node.parent.getSymbolTable().addSymbol(
1,563✔
137
                        node.tokens.name.text,
138
                        { definingNode: node },
139
                        funcType,
140
                        SymbolTypeFlag.runtime
141
                    );
142
                }
143

144
                const namespace = node.findAncestor(isNamespaceStatement);
1,563✔
145
                //this function is declared inside a namespace
146
                if (namespace) {
1,563✔
147
                    namespace.getSymbolTable().addSymbol(
371✔
148
                        node.tokens.name?.text,
1,113!
149
                        { definingNode: node },
150
                        funcType,
151
                        SymbolTypeFlag.runtime
152
                    );
153
                    //add the transpiled name for namespaced functions to the root symbol table
154
                    const transpiledNamespaceFunctionName = node.getName(ParseMode.BrightScript);
371✔
155

156
                    this.event.file.parser.ast.symbolTable.addSymbol(
371✔
157
                        transpiledNamespaceFunctionName,
158
                        { definingNode: node },
159
                        funcType,
160
                        // eslint-disable-next-line no-bitwise
161
                        SymbolTypeFlag.runtime | SymbolTypeFlag.postTranspile
162
                    );
163
                }
164
            },
165
            FunctionExpression: (node) => {
166
                const funcSymbolTable = node.getSymbolTable();
1,822✔
167
                const isInlineFunc = !(isFunctionStatement(node.parent) || isMethodStatement(node.parent));
1,822✔
168
                if (!funcSymbolTable?.hasSymbol('m', SymbolTypeFlag.runtime) || isInlineFunc) {
1,822!
169
                    if (!isTypecastStatement(node.body?.statements?.[0])) {
27!
170
                        funcSymbolTable?.addSymbol('m', { isInstance: true }, new AssociativeArrayType(), SymbolTypeFlag.runtime);
26!
171
                    }
172
                }
173
                this.validateFunctionParameterCount(node);
1,822✔
174
            },
175
            FunctionParameterExpression: (node) => {
176
                const paramName = node.tokens.name?.text;
966!
177
                const data: ExtraSymbolData = {};
966✔
178
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime, data: data });
966✔
179
                // add param symbol at expression level, so it can be used as default value in other params
180
                const funcExpr = node.findAncestor<FunctionExpression>(isFunctionExpression);
966✔
181
                const funcSymbolTable = funcExpr?.getSymbolTable();
966!
182
                funcSymbolTable?.addSymbol(paramName, { definingNode: node, isInstance: true, isFromDocComment: data.isFromDocComment }, nodeType, SymbolTypeFlag.runtime);
966!
183

184
                //also add param symbol at block level, as it may be redefined, and if so, should show a union
185
                funcExpr.body.getSymbolTable()?.addSymbol(paramName, { definingNode: node, isInstance: true, isFromDocComment: data.isFromDocComment }, nodeType, SymbolTypeFlag.runtime);
966!
186
            },
187
            InterfaceStatement: (node) => {
188
                this.validateDeclarationLocations(node, 'interface', () => util.createBoundingRange(node.tokens.interface, node.tokens.name));
122✔
189

190
                const nodeType = node.getType({ flags: SymbolTypeFlag.typetime });
122✔
191
                // eslint-disable-next-line no-bitwise
192
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node }, nodeType, SymbolTypeFlag.typetime);
122✔
193
            },
194
            ConstStatement: (node) => {
195
                this.validateDeclarationLocations(node, 'const', () => util.createBoundingRange(node.tokens.const, node.tokens.name));
140✔
196
                const nodeType = node.getType({ flags: SymbolTypeFlag.runtime });
140✔
197
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true }, nodeType, SymbolTypeFlag.runtime);
140✔
198
            },
199
            CatchStatement: (node) => {
200
                node.parent.getSymbolTable().addSymbol(node.tokens.exceptionVariable.text, { definingNode: node, isInstance: true }, DynamicType.instance, SymbolTypeFlag.runtime);
6✔
201
            },
202
            DimStatement: (node) => {
203
                if (node.tokens.name) {
18!
204
                    node.parent.getSymbolTable().addSymbol(node.tokens.name.text, { definingNode: node, isInstance: true }, node.getType({ flags: SymbolTypeFlag.runtime }), SymbolTypeFlag.runtime);
18✔
205
                }
206
            },
207
            ContinueStatement: (node) => {
208
                this.validateContinueStatement(node);
8✔
209
            },
210
            TypecastStatement: (node) => {
211
                node.parent.getSymbolTable().addSymbol('m', { definingNode: node, doNotMerge: true, isInstance: true }, node.getType({ flags: SymbolTypeFlag.typetime }), SymbolTypeFlag.runtime);
19✔
212
            },
213
            ConditionalCompileConstStatement: (node) => {
214
                const assign = node.assignment;
10✔
215
                const constNameLower = assign.tokens.name?.text.toLowerCase();
10!
216
                const astBsConsts = this.event.file.ast.bsConsts;
10✔
217
                if (isLiteralExpression(assign.value)) {
10!
218
                    astBsConsts.set(constNameLower, assign.value.tokens.value.text.toLowerCase() === 'true');
10✔
UNCOV
219
                } else if (isVariableExpression(assign.value)) {
×
UNCOV
220
                    if (this.validateConditionalCompileConst(assign.value.tokens.name)) {
×
UNCOV
221
                        astBsConsts.set(constNameLower, astBsConsts.get(assign.value.tokens.name.text.toLowerCase()));
×
222
                    }
223
                }
224
            },
225
            ConditionalCompileStatement: (node) => {
226
                this.validateConditionalCompileConst(node.tokens.condition);
22✔
227
            },
228
            ConditionalCompileErrorStatement: (node) => {
229
                this.event.program.diagnostics.register({
1✔
230
                    ...DiagnosticMessages.hashError(node.tokens.message.text),
231
                    location: node.location
232
                });
233
            },
234
            AliasStatement: (node) => {
235
                // eslint-disable-next-line no-bitwise
236
                const targetType = node.value.getType({ flags: SymbolTypeFlag.typetime | SymbolTypeFlag.runtime });
30✔
237

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

241
            }
242
        });
243

244
        this.event.file.ast.walk((node, parent) => {
1,595✔
245
            visitor(node, parent);
22,275✔
246
        }, {
247
            walkMode: WalkMode.visitAllRecursive
248
        });
249
    }
250

251
    /**
252
     * Validate that a statement is defined in one of these specific locations
253
     *  - the root of the AST
254
     *  - inside a namespace
255
     * This is applicable to things like FunctionStatement, ClassStatement, NamespaceStatement, EnumStatement, InterfaceStatement
256
     */
257
    private validateDeclarationLocations(statement: Statement, keyword: string, rangeFactory?: () => (Range | undefined)) {
258
        //if nested inside a namespace, or defined at the root of the AST (i.e. in a body that has no parent)
259
        const isOkDeclarationLocation = (parentNode) => {
2,923✔
260
            return isNamespaceStatement(parentNode?.parent) || (isBody(parentNode) && !parentNode?.parent);
2,928!
261
        };
262
        if (isOkDeclarationLocation(statement.parent)) {
2,923✔
263
            return;
2,905✔
264
        }
265

266
        // is this in a top levelconditional compile?
267
        if (isConditionalCompileStatement(statement.parent?.parent)) {
18!
268
            if (isOkDeclarationLocation(statement.parent.parent.parent)) {
5✔
269
                return;
4✔
270
            }
271
        }
272

273
        //the statement was defined in the wrong place. Flag it.
274
        this.event.program.diagnostics.register({
14✔
275
            ...DiagnosticMessages.keywordMustBeDeclaredAtNamespaceLevel(keyword),
276
            location: rangeFactory ? util.createLocationFromFileRange(this.event.file, rangeFactory()) : statement.location
14!
277
        });
278
    }
279

280
    private validateFunctionParameterCount(func: FunctionExpression) {
281
        if (func.parameters.length > CallExpression.MaximumArguments) {
1,822✔
282
            //flag every parameter over the limit
283
            for (let i = CallExpression.MaximumArguments; i < func.parameters.length; i++) {
2✔
284
                this.event.program.diagnostics.register({
3✔
285
                    ...DiagnosticMessages.tooManyCallableParameters(func.parameters.length, CallExpression.MaximumArguments),
286
                    location: func.parameters[i]?.tokens.name?.location ?? func.parameters[i]?.location ?? func.location
36!
287
                });
288
            }
289
        }
290
    }
291

292
    private validateEnumDeclaration(stmt: EnumStatement) {
293
        const members = stmt.getMembers();
133✔
294
        //the enum data type is based on the first member value
295
        const enumValueKind = (members.find(x => x.value)?.value as LiteralExpression)?.tokens?.value?.kind ?? TokenKind.IntegerLiteral;
187✔
296
        const memberNames = new Set<string>();
133✔
297
        for (const member of members) {
133✔
298
            const memberNameLower = member.name?.toLowerCase();
258!
299

300
            /**
301
             * flag duplicate member names
302
             */
303
            if (memberNames.has(memberNameLower)) {
258✔
304
                this.event.program.diagnostics.register({
1✔
305
                    ...DiagnosticMessages.duplicateIdentifier(member.name),
306
                    location: member.location
307
                });
308
            } else {
309
                memberNames.add(memberNameLower);
257✔
310
            }
311

312
            //Enforce all member values are the same type
313
            this.validateEnumValueTypes(member, enumValueKind);
258✔
314
        }
315
    }
316

317
    private validateEnumValueTypes(member: EnumMemberStatement, enumValueKind: TokenKind) {
318
        let memberValueKind: TokenKind;
319
        let memberValue: Expression;
320
        if (isUnaryExpression(member.value)) {
258✔
321
            memberValueKind = (member.value?.right as LiteralExpression)?.tokens?.value?.kind;
2!
322
            memberValue = member.value?.right;
2!
323
        } else {
324
            memberValueKind = (member.value as LiteralExpression)?.tokens?.value?.kind;
256✔
325
            memberValue = member.value;
256✔
326
        }
327
        const range = (memberValue ?? member)?.location?.range;
258!
328
        if (
258✔
329
            //is integer enum, has value, that value type is not integer
330
            (enumValueKind === TokenKind.IntegerLiteral && memberValueKind && memberValueKind !== enumValueKind) ||
859✔
331
            //has value, that value is not a literal
332
            (memberValue && !isLiteralExpression(memberValue))
333
        ) {
334
            this.event.program.diagnostics.register({
6✔
335
                ...DiagnosticMessages.enumValueMustBeType(
336
                    enumValueKind.replace(/literal$/i, '').toLowerCase()
337
                ),
338
                location: util.createLocationFromFileRange(this.event.file, range)
339
            });
340
        }
341

342
        //is non integer value
343
        if (enumValueKind !== TokenKind.IntegerLiteral) {
258✔
344
            //default value present
345
            if (memberValueKind) {
93✔
346
                //member value is same as enum
347
                if (memberValueKind !== enumValueKind) {
91✔
348
                    this.event.program.diagnostics.register({
1✔
349
                        ...DiagnosticMessages.enumValueMustBeType(
350
                            enumValueKind.replace(/literal$/i, '').toLowerCase()
351
                        ),
352
                        location: util.createLocationFromFileRange(this.event.file, range)
353
                    });
354
                }
355

356
                //default value missing
357
            } else {
358
                this.event.program.diagnostics.register({
2✔
359
                    ...DiagnosticMessages.enumValueIsRequired(
360
                        enumValueKind.replace(/literal$/i, '').toLowerCase()
361
                    ),
362
                    location: util.createLocationFromFileRange(this.event.file, range)
363
                });
364
            }
365
        }
366
    }
367

368

369
    private validateConditionalCompileConst(ccConst: Token) {
370
        const isBool = ccConst.kind === TokenKind.True || ccConst.kind === TokenKind.False;
22✔
371
        if (!isBool && !this.event.file.ast.bsConsts.has(ccConst.text.toLowerCase())) {
22✔
372
            this.event.program.diagnostics.register({
2✔
373
                ...DiagnosticMessages.referencedConstDoesNotExist(),
374
                location: ccConst.location
375
            });
376
            return false;
2✔
377
        }
378
        return true;
20✔
379
    }
380

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

415
    private getTopOfFileStatements() {
416
        let topOfFileIncludeStatements = [] as Array<LibraryStatement | ImportStatement | TypecastStatement | AliasStatement>;
3,188✔
417
        for (let stmt of this.event.file.parser.ast.statements) {
3,188✔
418
            //if we found a non-library statement, this statement is not at the top of the file
419
            if (isLibraryStatement(stmt) || isImportStatement(stmt) || isTypecastStatement(stmt) || isAliasStatement(stmt)) {
3,510✔
420
                topOfFileIncludeStatements.push(stmt);
452✔
421
            } else {
422
                //break out of the loop, we found all of our library statements
423
                break;
3,058✔
424
            }
425
        }
426
        return topOfFileIncludeStatements;
3,188✔
427
    }
428

429
    private validateTopOfFileStatements() {
430
        let topOfFileStatements = this.getTopOfFileStatements();
1,594✔
431

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

464
    private validateTypecastStatements() {
465
        let topOfFileTypecastStatements = this.getTopOfFileStatements().filter(stmt => isTypecastStatement(stmt));
1,594✔
466

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

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

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

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

500
            if (!isFirst || !isAllowedBlock) {
12✔
501
                this.event.program.diagnostics.register({
3✔
502
                    ...DiagnosticMessages.typecastStatementMustBeDeclaredAtStart(),
503
                    location: result.location
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
                    location: statement.tokens.loopType.location,
517
                    ...DiagnosticMessages.expectedToken(expectedLoopType)
518
                });
519
            }
520
        };
521

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

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

570
                this.event.program.diagnostics.register({
8✔
571
                    ...DiagnosticMessages.noOptionalChainingInLeftHandSideOfAssignment(),
572
                    location: util.createLocationFromFileRange(this.event.file, range)
573
                });
574
            }
575

576
            if (node === parent) {
200✔
577
                break;
100✔
578
            } else {
579
                nodes.push(node.parent);
100✔
580
            }
581
        }
582
    }
583
}
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