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

rokucommunity / brighterscript / #13071

25 Sep 2024 04:16PM UTC coverage: 86.525% (-1.4%) from 87.933%
#13071

push

web-flow
Merge c610b9e4e into 56dcaaa63

10903 of 13389 branches covered (81.43%)

Branch coverage included in aggregate %.

6936 of 7533 new or added lines in 100 files covered. (92.07%)

83 existing lines in 18 files now uncovered.

12548 of 13714 relevant lines covered (91.5%)

27591.0 hits per line

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

89.37
/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
import type { BrightScriptDoc } from '../../parser/BrightScriptDocParser';
19
import brsDocParser from '../../parser/BrightScriptDocParser';
1✔
20

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

27

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

31
        util.validateTooDeepFile(this.event.file);
1,599✔
32

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

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

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

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

53
    /**
54
     * Walk the full AST
55
     */
56
    private walk() {
57

58

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

79
                this.validateEnumDeclaration(node);
133✔
80

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

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

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

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

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

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

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

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

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

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

243
            },
244
            AstNode: (node) => {
245
                //check for doc comments
246
                if (!node.leadingTrivia || node.leadingTrivia.length === 0) {
22,307✔
247
                    return;
3,947✔
248
                }
249
                const doc = brsDocParser.parseNode(node);
18,360✔
250
                if (doc.tags.length === 0) {
18,360✔
251
                    return;
18,324✔
252
                }
253

254
                let funcExpr = node.findAncestor<FunctionExpression>(isFunctionExpression);
36✔
255
                if (funcExpr) {
36✔
256
                    // handle comment tags inside a function expression
257
                    this.processDocTagsInFunction(doc, node, funcExpr);
8✔
258
                } else {
259
                    //handle comment tags outside of a function expression
260
                    this.processDocTagsAtTopLevel(doc, node);
28✔
261
                }
262
            }
263
        });
264

265
        this.event.file.ast.walk((node, parent) => {
1,599✔
266
            visitor(node, parent);
22,307✔
267
        }, {
268
            walkMode: WalkMode.visitAllRecursive
269
        });
270
    }
271

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

275
        // For example, declaring variable types:
276
        // const symbolTable = funcExpr.body.getSymbolTable();
277

278
        // for (const varTag of doc.getAllTags(BrsDocTagKind.Var)) {
279
        //     const varName = (varTag as BrsDocParamTag).name;
280
        //     const varTypeStr = (varTag as BrsDocParamTag).type;
281
        //     const data: ExtraSymbolData = {};
282
        //     const type = doc.getTypeFromContext(varTypeStr, node, { flags: SymbolTypeFlag.typetime, fullName: varTypeStr, data: data, tableProvider: () => symbolTable });
283
        //     if (type) {
284
        //         symbolTable.addSymbol(varName, { ...data, isFromDocComment: true }, type, SymbolTypeFlag.runtime);
285
        //     }
286
        // }
287
    }
288

289
    private processDocTagsAtTopLevel(doc: BrightScriptDoc, node: AstNode) {
290
        //TODO:
291
        // - handle import statements?
292
        // - handle library statements?
293
        // - handle typecast statements?
294
        // - handle alias statements?
295
        // - handle const statements?
296
        // - allow interface definitions?
297
    }
298

299
    /**
300
     * Validate that a statement is defined in one of these specific locations
301
     *  - the root of the AST
302
     *  - inside a namespace
303
     * This is applicable to things like FunctionStatement, ClassStatement, NamespaceStatement, EnumStatement, InterfaceStatement
304
     */
305
    private validateDeclarationLocations(statement: Statement, keyword: string, rangeFactory?: () => (Range | undefined)) {
306
        //if nested inside a namespace, or defined at the root of the AST (i.e. in a body that has no parent)
307
        const isOkDeclarationLocation = (parentNode) => {
2,927✔
308
            return isNamespaceStatement(parentNode?.parent) || (isBody(parentNode) && !parentNode?.parent);
2,932!
309
        };
310
        if (isOkDeclarationLocation(statement.parent)) {
2,927✔
311
            return;
2,909✔
312
        }
313

314
        // is this in a top levelconditional compile?
315
        if (isConditionalCompileStatement(statement.parent?.parent)) {
18!
316
            if (isOkDeclarationLocation(statement.parent.parent.parent)) {
5✔
317
                return;
4✔
318
            }
319
        }
320

321
        //the statement was defined in the wrong place. Flag it.
322
        this.event.program.diagnostics.register({
14✔
323
            ...DiagnosticMessages.keywordMustBeDeclaredAtNamespaceLevel(keyword),
324
            location: rangeFactory ? util.createLocationFromFileRange(this.event.file, rangeFactory()) : statement.location
14!
325
        });
326
    }
327

328
    private validateFunctionParameterCount(func: FunctionExpression) {
329
        if (func.parameters.length > CallExpression.MaximumArguments) {
1,826✔
330
            //flag every parameter over the limit
331
            for (let i = CallExpression.MaximumArguments; i < func.parameters.length; i++) {
2✔
332
                this.event.program.diagnostics.register({
3✔
333
                    ...DiagnosticMessages.tooManyCallableParameters(func.parameters.length, CallExpression.MaximumArguments),
334
                    location: func.parameters[i]?.tokens.name?.location ?? func.parameters[i]?.location ?? func.location
36!
335
                });
336
            }
337
        }
338
    }
339

340
    private validateEnumDeclaration(stmt: EnumStatement) {
341
        const members = stmt.getMembers();
133✔
342
        //the enum data type is based on the first member value
343
        const enumValueKind = (members.find(x => x.value)?.value as LiteralExpression)?.tokens?.value?.kind ?? TokenKind.IntegerLiteral;
187✔
344
        const memberNames = new Set<string>();
133✔
345
        for (const member of members) {
133✔
346
            const memberNameLower = member.name?.toLowerCase();
258!
347

348
            /**
349
             * flag duplicate member names
350
             */
351
            if (memberNames.has(memberNameLower)) {
258✔
352
                this.event.program.diagnostics.register({
1✔
353
                    ...DiagnosticMessages.duplicateIdentifier(member.name),
354
                    location: member.location
355
                });
356
            } else {
357
                memberNames.add(memberNameLower);
257✔
358
            }
359

360
            //Enforce all member values are the same type
361
            this.validateEnumValueTypes(member, enumValueKind);
258✔
362
        }
363
    }
364

365
    private validateEnumValueTypes(member: EnumMemberStatement, enumValueKind: TokenKind) {
366
        let memberValueKind: TokenKind;
367
        let memberValue: Expression;
368
        if (isUnaryExpression(member.value)) {
258✔
369
            memberValueKind = (member.value?.right as LiteralExpression)?.tokens?.value?.kind;
2!
370
            memberValue = member.value?.right;
2!
371
        } else {
372
            memberValueKind = (member.value as LiteralExpression)?.tokens?.value?.kind;
256✔
373
            memberValue = member.value;
256✔
374
        }
375
        const range = (memberValue ?? member)?.location?.range;
258!
376
        if (
258✔
377
            //is integer enum, has value, that value type is not integer
378
            (enumValueKind === TokenKind.IntegerLiteral && memberValueKind && memberValueKind !== enumValueKind) ||
859✔
379
            //has value, that value is not a literal
380
            (memberValue && !isLiteralExpression(memberValue))
381
        ) {
382
            this.event.program.diagnostics.register({
6✔
383
                ...DiagnosticMessages.enumValueMustBeType(
384
                    enumValueKind.replace(/literal$/i, '').toLowerCase()
385
                ),
386
                location: util.createLocationFromFileRange(this.event.file, range)
387
            });
388
        }
389

390
        //is non integer value
391
        if (enumValueKind !== TokenKind.IntegerLiteral) {
258✔
392
            //default value present
393
            if (memberValueKind) {
93✔
394
                //member value is same as enum
395
                if (memberValueKind !== enumValueKind) {
91✔
396
                    this.event.program.diagnostics.register({
1✔
397
                        ...DiagnosticMessages.enumValueMustBeType(
398
                            enumValueKind.replace(/literal$/i, '').toLowerCase()
399
                        ),
400
                        location: util.createLocationFromFileRange(this.event.file, range)
401
                    });
402
                }
403

404
                //default value missing
405
            } else {
406
                this.event.program.diagnostics.register({
2✔
407
                    ...DiagnosticMessages.enumValueIsRequired(
408
                        enumValueKind.replace(/literal$/i, '').toLowerCase()
409
                    ),
410
                    location: util.createLocationFromFileRange(this.event.file, range)
411
                });
412
            }
413
        }
414
    }
415

416

417
    private validateConditionalCompileConst(ccConst: Token) {
418
        const isBool = ccConst.kind === TokenKind.True || ccConst.kind === TokenKind.False;
22✔
419
        if (!isBool && !this.event.file.ast.bsConsts.has(ccConst.text.toLowerCase())) {
22✔
420
            this.event.program.diagnostics.register({
2✔
421
                ...DiagnosticMessages.referencedConstDoesNotExist(),
422
                location: ccConst.location
423
            });
424
            return false;
2✔
425
        }
426
        return true;
20✔
427
    }
428

429
    /**
430
     * Find statements defined at the top level (or inside a namespace body) that are not allowed to be there
431
     */
432
    private flagTopLevelStatements() {
433
        const statements = [...this.event.file.ast.statements];
1,599✔
434
        while (statements.length > 0) {
1,599✔
435
            const statement = statements.pop();
3,155✔
436
            if (isNamespaceStatement(statement)) {
3,155✔
437
                statements.push(...statement.body.statements);
558✔
438
            } else {
439
                //only allow these statement types
440
                if (
2,597✔
441
                    !isFunctionStatement(statement) &&
5,942✔
442
                    !isClassStatement(statement) &&
443
                    !isEnumStatement(statement) &&
444
                    !isInterfaceStatement(statement) &&
445
                    !isLibraryStatement(statement) &&
446
                    !isImportStatement(statement) &&
447
                    !isConstStatement(statement) &&
448
                    !isTypecastStatement(statement) &&
449
                    !isConditionalCompileConstStatement(statement) &&
450
                    !isConditionalCompileErrorStatement(statement) &&
451
                    !isConditionalCompileStatement(statement) &&
452
                    !isAliasStatement(statement)
453
                ) {
454
                    this.event.program.diagnostics.register({
7✔
455
                        ...DiagnosticMessages.unexpectedStatementOutsideFunction(),
456
                        location: statement.location
457
                    });
458
                }
459
            }
460
        }
461
    }
462

463
    private getTopOfFileStatements() {
464
        let topOfFileIncludeStatements = [] as Array<LibraryStatement | ImportStatement | TypecastStatement | AliasStatement>;
3,196✔
465
        for (let stmt of this.event.file.parser.ast.statements) {
3,196✔
466
            //if we found a non-library statement, this statement is not at the top of the file
467
            if (isLibraryStatement(stmt) || isImportStatement(stmt) || isTypecastStatement(stmt) || isAliasStatement(stmt)) {
3,518✔
468
                topOfFileIncludeStatements.push(stmt);
452✔
469
            } else {
470
                //break out of the loop, we found all of our library statements
471
                break;
3,066✔
472
            }
473
        }
474
        return topOfFileIncludeStatements;
3,196✔
475
    }
476

477
    private validateTopOfFileStatements() {
478
        let topOfFileStatements = this.getTopOfFileStatements();
1,598✔
479

480
        let statements = [
1,598✔
481
            // eslint-disable-next-line @typescript-eslint/dot-notation
482
            ...this.event.file['_cachedLookups'].libraryStatements,
483
            // eslint-disable-next-line @typescript-eslint/dot-notation
484
            ...this.event.file['_cachedLookups'].importStatements,
485
            // eslint-disable-next-line @typescript-eslint/dot-notation
486
            ...this.event.file['_cachedLookups'].aliasStatements
487
        ];
488
        for (let result of statements) {
1,598✔
489
            //if this statement is not one of the top-of-file statements,
490
            //then add a diagnostic explaining that it is invalid
491
            if (!topOfFileStatements.includes(result)) {
224✔
492
                if (isLibraryStatement(result)) {
5✔
493
                    this.event.program.diagnostics.register({
2✔
494
                        ...DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('library'),
495
                        location: result.location
496
                    });
497
                } else if (isImportStatement(result)) {
3✔
498
                    this.event.program.diagnostics.register({
1✔
499
                        ...DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('import'),
500
                        location: result.location
501
                    });
502
                } else if (isAliasStatement(result)) {
2!
503
                    this.event.program.diagnostics.register({
2✔
504
                        ...DiagnosticMessages.statementMustBeDeclaredAtTopOfFile('alias'),
505
                        location: result.location
506
                    });
507
                }
508
            }
509
        }
510
    }
511

512
    private validateTypecastStatements() {
513
        let topOfFileTypecastStatements = this.getTopOfFileStatements().filter(stmt => isTypecastStatement(stmt));
1,598✔
514

515
        //check only one `typecast` statement at "top" of file (eg. before non import/library statements)
516
        for (let i = 1; i < topOfFileTypecastStatements.length; i++) {
1,598✔
517
            const typecastStmt = topOfFileTypecastStatements[i];
1✔
518
            this.event.program.diagnostics.register({
1✔
519
                ...DiagnosticMessages.typecastStatementMustBeDeclaredAtStart(),
520
                location: typecastStmt.location
521
            });
522
        }
523

524
        // eslint-disable-next-line @typescript-eslint/dot-notation
525
        for (let result of this.event.file['_cachedLookups'].typecastStatements) {
1,598✔
526
            let isBadTypecastObj = false;
19✔
527
            if (!isVariableExpression(result.typecastExpression.obj)) {
19✔
528
                isBadTypecastObj = true;
1✔
529
            } else if (result.typecastExpression.obj.tokens.name.text.toLowerCase() !== 'm') {
18✔
530
                isBadTypecastObj = true;
1✔
531
            }
532
            if (isBadTypecastObj) {
19✔
533
                this.event.program.diagnostics.register({
2✔
534
                    ...DiagnosticMessages.invalidTypecastStatementApplication(util.getAllDottedGetPartsAsString(result.typecastExpression.obj)),
535
                    location: result.typecastExpression.obj.location
536
                });
537
            }
538

539
            if (topOfFileTypecastStatements.includes(result)) {
19✔
540
                // already validated
541
                continue;
7✔
542
            }
543

544
            const block = result.findAncestor<Body | Block>(node => (isBody(node) || isBlock(node)));
12✔
545
            const isFirst = block?.statements[0] === result;
12!
546
            const isAllowedBlock = (isBody(block) || isFunctionExpression(block.parent) || isNamespaceStatement(block.parent));
12!
547

548
            if (!isFirst || !isAllowedBlock) {
12✔
549
                this.event.program.diagnostics.register({
3✔
550
                    ...DiagnosticMessages.typecastStatementMustBeDeclaredAtStart(),
551
                    location: result.location
552
                });
553
            }
554
        }
555
    }
556

557
    private validateContinueStatement(statement: ContinueStatement) {
558
        const validateLoopTypeMatch = (expectedLoopType: TokenKind) => {
8✔
559
            //coerce ForEach to For
560
            expectedLoopType = expectedLoopType === TokenKind.ForEach ? TokenKind.For : expectedLoopType;
7✔
561
            const actualLoopType = statement.tokens.loopType;
7✔
562
            if (actualLoopType && expectedLoopType?.toLowerCase() !== actualLoopType.text?.toLowerCase()) {
7!
563
                this.event.program.diagnostics.register({
3✔
564
                    location: statement.tokens.loopType.location,
565
                    ...DiagnosticMessages.expectedToken(expectedLoopType)
566
                });
567
            }
568
        };
569

570
        //find the parent loop statement
571
        const parent = statement.findAncestor<WhileStatement | ForStatement | ForEachStatement>((node) => {
8✔
572
            if (isWhileStatement(node)) {
18✔
573
                validateLoopTypeMatch(node.tokens.while.kind);
3✔
574
                return true;
3✔
575
            } else if (isForStatement(node)) {
15✔
576
                validateLoopTypeMatch(node.tokens.for.kind);
3✔
577
                return true;
3✔
578
            } else if (isForEachStatement(node)) {
12✔
579
                validateLoopTypeMatch(node.tokens.forEach.kind);
1✔
580
                return true;
1✔
581
            }
582
        });
583
        //flag continue statements found outside of a loop
584
        if (!parent) {
8✔
585
            this.event.program.diagnostics.register({
1✔
586
                location: statement.location,
587
                ...DiagnosticMessages.illegalContinueStatement()
588
            });
589
        }
590
    }
591

592
    /**
593
     * Validate that there are no optional chaining operators on the left-hand-side of an assignment, indexed set, or dotted get
594
     */
595
    private validateNoOptionalChainingInVarSet(parent: AstNode, children: AstNode[]) {
596
        const nodes = [...children, parent];
100✔
597
        //flag optional chaining anywhere in the left of this statement
598
        while (nodes.length > 0) {
100✔
599
            const node = nodes.shift();
200✔
600
            if (
200✔
601
                // a?.b = true or a.b?.c = true
602
                ((isDottedSetStatement(node) || isDottedGetExpression(node)) && node.tokens.dot?.kind === TokenKind.QuestionDot) ||
1,140!
603
                // a.b?[2] = true
604
                (isIndexedGetExpression(node) && (node?.tokens.questionDot?.kind === TokenKind.QuestionDot || node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare)) ||
36!
605
                // a?[1] = true
606
                (isIndexedSetStatement(node) && node.tokens.openingSquare?.kind === TokenKind.QuestionLeftSquare)
45!
607
            ) {
608
                //try to highlight the entire left-hand-side expression if possible
609
                let range: Range;
610
                if (isDottedSetStatement(parent)) {
8✔
611
                    range = util.createBoundingRange(parent.obj?.location, parent.tokens.dot, parent.tokens.name);
5!
612
                } else if (isIndexedSetStatement(parent)) {
3!
613
                    range = util.createBoundingRange(parent.obj?.location, parent.tokens.openingSquare, ...parent.indexes, parent.tokens.closingSquare);
3!
614
                } else {
NEW
615
                    range = node.location?.range;
×
616
                }
617

618
                this.event.program.diagnostics.register({
8✔
619
                    ...DiagnosticMessages.noOptionalChainingInLeftHandSideOfAssignment(),
620
                    location: util.createLocationFromFileRange(this.event.file, range)
621
                });
622
            }
623

624
            if (node === parent) {
200✔
625
                break;
100✔
626
            } else {
627
                nodes.push(node.parent);
100✔
628
            }
629
        }
630
    }
631
}
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