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

rokucommunity / brighterscript / #13234

28 Oct 2024 07:06PM UTC coverage: 89.066% (+2.2%) from 86.866%
#13234

push

web-flow
Merge 9bcb77aad into 9ec6f722c

7233 of 8558 branches covered (84.52%)

Branch coverage included in aggregate %.

34 of 34 new or added lines in 5 files covered. (100.0%)

543 existing lines in 53 files now uncovered.

9621 of 10365 relevant lines covered (92.82%)

1782.52 hits per line

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

91.88
/src/bscPlugin/validation/BrsFileValidator.ts
1
import { isBody, isClassStatement, isCommentStatement, isConstStatement, isDottedGetExpression, isDottedSetStatement, isEnumStatement, isForEachStatement, isForStatement, isFunctionStatement, isImportStatement, isIndexedGetExpression, isIndexedSetStatement, isInterfaceStatement, isLibraryStatement, isLiteralExpression, isNamespaceStatement, isUnaryExpression, 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 { 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, WhileStatement } from '../../parser/Statement';
11
import { DynamicType } from '../../types/DynamicType';
1✔
12
import { InterfaceType } from '../../types/InterfaceType';
1✔
13
import util from '../../util';
1✔
14
import type { Range } from 'vscode-languageserver';
15

16
export class BrsFileValidator {
1✔
17
    constructor(
18
        public event: OnFileValidateEvent<BrsFile>
766✔
19
    ) {
20
    }
21

22
    public process() {
23
        util.validateTooDeepFile(this.event.file);
766✔
24
        this.walk();
766✔
25
        this.flagTopLevelStatements();
764✔
26
        //only validate the file if it was actually parsed (skip files containing typedefs)
27
        if (!this.event.file.hasTypedef) {
764✔
28
            this.validateImportStatements();
763✔
29
        }
30
    }
31

32
    /**
33
     * Walk the full AST
34
     */
35
    private walk() {
36
        const visitor = createVisitor({
766✔
37
            MethodStatement: (node) => {
38
                //add the `super` symbol to class methods
39
                node.func.body.symbolTable.addSymbol('super', undefined, DynamicType.instance);
125✔
40
            },
41
            CallfuncExpression: (node) => {
42
                if (node.args.length > 5) {
14✔
43
                    this.event.file.addDiagnostic({
1✔
44
                        ...DiagnosticMessages.callfuncHasToManyArgs(node.args.length),
45
                        range: node.methodName.range
46
                    });
47
                }
48
            },
49
            EnumStatement: (node) => {
50
                this.validateDeclarationLocations(node, 'enum', () => util.createBoundingRange(node.tokens.enum, node.tokens.name));
72✔
51

52
                this.validateEnumDeclaration(node);
72✔
53

54
                //register this enum declaration
55
                node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, node.tokens.name.range, DynamicType.instance);
72!
56
            },
57
            ClassStatement: (node) => {
58
                this.validateDeclarationLocations(node, 'class', () => util.createBoundingRange(node.classKeyword, node.name));
191✔
59

60
                //register this class
61
                node.parent.getSymbolTable()?.addSymbol(node.name.text, node.name.range, DynamicType.instance);
191!
62
            },
63
            AssignmentStatement: (node) => {
64
                //register this variable
65
                node.parent.getSymbolTable()?.addSymbol(node.name.text, node.name.range, DynamicType.instance);
353!
66
            },
67
            DottedSetStatement: (node) => {
68
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
18✔
69
            },
70
            IndexedSetStatement: (node) => {
71
                this.validateNoOptionalChainingInVarSet(node, [node.obj]);
15✔
72
            },
73
            ForEachStatement: (node) => {
74
                //register the for loop variable
75
                node.parent.getSymbolTable()?.addSymbol(node.item.text, node.item.range, DynamicType.instance);
8!
76
            },
77
            NamespaceStatement: (node) => {
78
                this.validateDeclarationLocations(node, 'namespace', () => util.createBoundingRange(node.keyword, node.nameExpression));
171✔
79

80
                node.parent.getSymbolTable().addSymbol(
171✔
81
                    node.name.split('.')[0],
82
                    node.nameExpression.range,
83
                    DynamicType.instance
84
                );
85
            },
86
            FunctionStatement: (node) => {
87
                this.validateDeclarationLocations(node, 'function', () => util.createBoundingRange(node.func.functionType, node.name));
710✔
88
                if (node.name?.text) {
710✔
89
                    node.parent.getSymbolTable().addSymbol(
709✔
90
                        node.name.text,
91
                        node.name.range,
92
                        DynamicType.instance
93
                    );
94
                }
95

96
                const namespace = node.findAncestor(isNamespaceStatement);
710✔
97
                //this function is declared inside a namespace
98
                if (namespace) {
710✔
99
                    //add the transpiled name for namespaced functions to the root symbol table
100
                    const transpiledNamespaceFunctionName = node.getName(ParseMode.BrightScript);
82✔
101
                    const funcType = node.func.getFunctionType();
82✔
102
                    funcType.setName(transpiledNamespaceFunctionName);
82✔
103

104
                    this.event.file.parser.ast.symbolTable.addSymbol(
82✔
105
                        transpiledNamespaceFunctionName,
106
                        node.name.range,
107
                        funcType
108
                    );
109
                }
110
            },
111
            FunctionExpression: (node) => {
112
                if (!node.symbolTable.hasSymbol('m')) {
848✔
113
                    node.symbolTable.addSymbol('m', undefined, DynamicType.instance);
832✔
114
                }
115
                this.validateFunctionParameterCount(node);
848✔
116
            },
117
            FunctionParameterExpression: (node) => {
118
                const paramName = node.name?.text;
501!
119
                const symbolTable = node.getSymbolTable();
501✔
120
                symbolTable?.addSymbol(paramName, node.name.range, node.type);
501!
121
            },
122
            InterfaceStatement: (node) => {
123
                this.validateDeclarationLocations(node, 'interface', () => util.createBoundingRange(node.tokens.interface, node.tokens.name));
30✔
124
                node.parent?.getSymbolTable()?.addSymbol(node.tokens.name.text, node.tokens.name.range, new InterfaceType(new Map()));
30!
125
            },
126
            ConstStatement: (node) => {
127
                this.validateDeclarationLocations(node, 'const', () => util.createBoundingRange(node.tokens.const, node.tokens.name));
39✔
128
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, node.tokens.name.range, DynamicType.instance);
39✔
129
            },
130
            CatchStatement: (node) => {
131
                node.parent.getSymbolTable().addSymbol(node.exceptionVariable.text, node.exceptionVariable.range, DynamicType.instance);
4✔
132
            },
133
            DimStatement: (node) => {
134
                if (node.identifier) {
18!
135
                    node.parent.getSymbolTable().addSymbol(node.identifier.text, node.identifier.range, DynamicType.instance);
18✔
136
                }
137
            },
138
            ContinueStatement: (node) => {
139
                this.validateContinueStatement(node);
8✔
140
            }
141
        });
142

143
        this.event.file.ast.walk((node, parent) => {
766✔
144
            visitor(node, parent);
9,325✔
145
        }, {
146
            walkMode: WalkMode.visitAllRecursive
147
        });
148
    }
149

150
    /**
151
     * Validate that a statement is defined in one of these specific locations
152
     *  - the root of the AST
153
     *  - inside a namespace
154
     * This is applicable to things like FunctionStatement, ClassStatement, NamespaceStatement, EnumStatement, InterfaceStatement
155
     */
156
    private validateDeclarationLocations(statement: Statement, keyword: string, rangeFactory?: () => (Range | undefined)) {
157
        //if nested inside a namespace, or defined at the root of the AST (i.e. in a body that has no parent)
158
        if (isNamespaceStatement(statement.parent?.parent) || (isBody(statement.parent) && !statement.parent?.parent)) {
1,213!
159
            return;
1,200✔
160
        }
161
        //the statement was defined in the wrong place. Flag it.
162
        this.event.file.addDiagnostic({
13✔
163
            ...DiagnosticMessages.keywordMustBeDeclaredAtNamespaceLevel(keyword),
164
            range: rangeFactory?.() ?? statement.range
78!
165
        });
166
    }
167

168
    private validateFunctionParameterCount(func: FunctionExpression) {
169
        if (func.parameters.length > CallExpression.MaximumArguments) {
848✔
170
            //flag every parameter over the limit
171
            for (let i = CallExpression.MaximumArguments; i < func.parameters.length; i++) {
2✔
172
                this.event.file.addDiagnostic({
3✔
173
                    ...DiagnosticMessages.tooManyCallableParameters(func.parameters.length, CallExpression.MaximumArguments),
174
                    range: func.parameters[i].name.range
175
                });
176
            }
177
        }
178
    }
179

180
    private validateEnumDeclaration(stmt: EnumStatement) {
181
        const members = stmt.getMembers();
72✔
182
        //the enum data type is based on the first member value
183
        const enumValueKind = (members.find(x => x.value)?.value as LiteralExpression)?.token?.kind ?? TokenKind.IntegerLiteral;
89✔
184
        const memberNames = new Set<string>();
72✔
185
        for (const member of members) {
72✔
186
            const memberNameLower = member.name?.toLowerCase();
119!
187

188
            /**
189
             * flag duplicate member names
190
             */
191
            if (memberNames.has(memberNameLower)) {
119✔
192
                this.event.file.addDiagnostic({
1✔
193
                    ...DiagnosticMessages.duplicateIdentifier(member.name),
194
                    range: member.range
195
                });
196
            } else {
197
                memberNames.add(memberNameLower);
118✔
198
            }
199

200
            //Enforce all member values are the same type
201
            this.validateEnumValueTypes(member, enumValueKind);
119✔
202
        }
203
    }
204

205
    private validateEnumValueTypes(member: EnumMemberStatement, enumValueKind: TokenKind) {
206
        let memberValueKind: TokenKind;
207
        let memberValue: Expression;
208
        if (isUnaryExpression(member.value)) {
119✔
209
            memberValueKind = (member.value?.right as LiteralExpression)?.token?.kind;
2!
210
            memberValue = member.value?.right;
2!
211
        } else {
212
            memberValueKind = (member.value as LiteralExpression)?.token?.kind;
117✔
213
            memberValue = member.value;
117✔
214
        }
215
        const range = (memberValue ?? member)?.range;
119!
216
        if (
119✔
217
            //is integer enum, has value, that value type is not integer
218
            (enumValueKind === TokenKind.IntegerLiteral && memberValueKind && memberValueKind !== enumValueKind) ||
393✔
219
            //has value, that value is not a literal
220
            (memberValue && !isLiteralExpression(memberValue))
221
        ) {
222
            this.event.file.addDiagnostic({
5✔
223
                ...DiagnosticMessages.enumValueMustBeType(
224
                    enumValueKind.replace(/literal$/i, '').toLowerCase()
225
                ),
226
                range: range
227
            });
228
        }
229

230
        //is non integer value
231
        if (enumValueKind !== TokenKind.IntegerLiteral) {
119✔
232
            //default value present
233
            if (memberValueKind) {
47✔
234
                //member value is same as enum
235
                if (memberValueKind !== enumValueKind) {
45✔
236
                    this.event.file.addDiagnostic({
1✔
237
                        ...DiagnosticMessages.enumValueMustBeType(
238
                            enumValueKind.replace(/literal$/i, '').toLowerCase()
239
                        ),
240
                        range: range
241
                    });
242
                }
243

244
                //default value missing
245
            } else {
246
                this.event.file.addDiagnostic({
2✔
247
                    file: this.event.file,
248
                    ...DiagnosticMessages.enumValueIsRequired(
249
                        enumValueKind.replace(/literal$/i, '').toLowerCase()
250
                    ),
251
                    range: range
252
                });
253
            }
254
        }
255
    }
256

257
    /**
258
     * Find statements defined at the top level (or inside a namespace body) that are not allowed to be there
259
     */
260
    private flagTopLevelStatements() {
261
        const statements = [...this.event.file.ast.statements];
764✔
262
        while (statements.length > 0) {
764✔
263
            const statement = statements.pop();
1,240✔
264
            if (isNamespaceStatement(statement)) {
1,240✔
265
                statements.push(...statement.body.statements);
167✔
266
            } else {
267
                //only allow these statement types
268
                if (
1,073✔
269
                    !isFunctionStatement(statement) &&
1,980✔
270
                    !isClassStatement(statement) &&
271
                    !isEnumStatement(statement) &&
272
                    !isInterfaceStatement(statement) &&
273
                    !isCommentStatement(statement) &&
274
                    !isLibraryStatement(statement) &&
275
                    !isImportStatement(statement) &&
276
                    !isConstStatement(statement)
277
                ) {
278
                    this.event.file.addDiagnostic({
6✔
279
                        ...DiagnosticMessages.unexpectedStatementOutsideFunction(),
280
                        range: statement.range
281
                    });
282
                }
283
            }
284
        }
285
    }
286

287
    private validateImportStatements() {
288
        let topOfFileIncludeStatements = [] as Array<LibraryStatement | ImportStatement>;
763✔
289
        for (let stmt of this.event.file.parser.ast.statements) {
763✔
290
            //skip comments
291
            if (isCommentStatement(stmt)) {
739✔
292
                continue;
7✔
293
            }
294
            //if we found a non-library statement, this statement is not at the top of the file
295
            if (isLibraryStatement(stmt) || isImportStatement(stmt)) {
732✔
296
                topOfFileIncludeStatements.push(stmt);
23✔
297
            } else {
298
                //break out of the loop, we found all of our library statements
299
                break;
709✔
300
            }
301
        }
302

303
        let statements = [
763✔
304
            // eslint-disable-next-line @typescript-eslint/dot-notation
305
            ...this.event.file['_parser'].references.libraryStatements,
306
            // eslint-disable-next-line @typescript-eslint/dot-notation
307
            ...this.event.file['_parser'].references.importStatements
308
        ];
309
        for (let result of statements) {
763✔
310
            //if this statement is not one of the top-of-file statements,
311
            //then add a diagnostic explaining that it is invalid
312
            if (!topOfFileIncludeStatements.includes(result)) {
26✔
313
                if (isLibraryStatement(result)) {
3✔
314
                    this.event.file.diagnostics.push({
2✔
315
                        ...DiagnosticMessages.libraryStatementMustBeDeclaredAtTopOfFile(),
316
                        range: result.range,
317
                        file: this.event.file
318
                    });
319
                } else if (isImportStatement(result)) {
1!
320
                    this.event.file.diagnostics.push({
1✔
321
                        ...DiagnosticMessages.importStatementMustBeDeclaredAtTopOfFile(),
322
                        range: result.range,
323
                        file: this.event.file
324
                    });
325
                }
326
            }
327
        }
328
    }
329

330
    private validateContinueStatement(statement: ContinueStatement) {
331
        const validateLoopTypeMatch = (expectedLoopType: TokenKind) => {
8✔
332
            //coerce ForEach to For
333
            expectedLoopType = expectedLoopType === TokenKind.ForEach ? TokenKind.For : expectedLoopType;
7✔
334
            const actualLoopType = statement.tokens.loopType;
7✔
335
            if (actualLoopType && expectedLoopType?.toLowerCase() !== actualLoopType.text?.toLowerCase()) {
7!
336
                this.event.file.addDiagnostic({
3✔
337
                    range: statement.tokens.loopType.range,
338
                    ...DiagnosticMessages.expectedToken(expectedLoopType)
339
                });
340
            }
341
        };
342

343
        //find the parent loop statement
344
        const parent = statement.findAncestor<WhileStatement | ForStatement | ForEachStatement>((node) => {
8✔
345
            if (isWhileStatement(node)) {
18✔
346
                validateLoopTypeMatch(node.tokens.while.kind);
3✔
347
                return true;
3✔
348
            } else if (isForStatement(node)) {
15✔
349
                validateLoopTypeMatch(node.forToken.kind);
3✔
350
                return true;
3✔
351
            } else if (isForEachStatement(node)) {
12✔
352
                validateLoopTypeMatch(node.tokens.forEach.kind);
1✔
353
                return true;
1✔
354
            }
355
        });
356
        //flag continue statements found outside of a loop
357
        if (!parent) {
8✔
358
            this.event.file.addDiagnostic({
1✔
359
                range: statement.range,
360
                ...DiagnosticMessages.illegalContinueStatement()
361
            });
362
        }
363
    }
364

365
    /**
366
     * Validate that there are no optional chaining operators on the left-hand-side of an assignment, indexed set, or dotted get
367
     */
368
    private validateNoOptionalChainingInVarSet(parent: AstNode, children: AstNode[]) {
369
        const nodes = [...children, parent];
33✔
370
        //flag optional chaining anywhere in the left of this statement
371
        while (nodes.length > 0) {
33✔
372
            const node = nodes.shift();
66✔
373
            if (
66✔
374
                // a?.b = true or a.b?.c = true
375
                ((isDottedSetStatement(node) || isDottedGetExpression(node)) && node.dot?.kind === TokenKind.QuestionDot) ||
369!
376
                // a.b?[2] = true
377
                (isIndexedGetExpression(node) && (node?.questionDotToken?.kind === TokenKind.QuestionDot || node.openingSquare?.kind === TokenKind.QuestionLeftSquare)) ||
27!
378
                // a?[1] = true
379
                (isIndexedSetStatement(node) && node.openingSquare?.kind === TokenKind.QuestionLeftSquare)
45!
380
            ) {
381
                //try to highlight the entire left-hand-side expression if possible
382
                let range: Range;
383
                if (isDottedSetStatement(parent)) {
8✔
384
                    range = util.createBoundingRange(parent.obj, parent.dot, parent.name);
5✔
385
                } else if (isIndexedSetStatement(parent)) {
3!
386
                    range = util.createBoundingRange(parent.obj, parent.openingSquare, parent.index, parent.closingSquare);
3✔
387
                } else {
UNCOV
388
                    range = node.range;
×
389
                }
390

391
                this.event.file.addDiagnostic({
8✔
392
                    ...DiagnosticMessages.noOptionalChainingInLeftHandSideOfAssignment(),
393
                    range: range
394
                });
395
            }
396

397
            if (node === parent) {
66✔
398
                break;
33✔
399
            } else {
400
                nodes.push(node.parent);
33✔
401
            }
402
        }
403
    }
404
}
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