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

rokucommunity / brighterscript / #15046

03 Oct 2022 01:55PM UTC coverage: 87.532% (-0.3%) from 87.808%
#15046

push

TwitchBronBron
0.59.0

5452 of 6706 branches covered (81.3%)

Branch coverage included in aggregate %.

8259 of 8958 relevant lines covered (92.2%)

1521.92 hits per line

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

93.24
/src/bscPlugin/validation/BrsFileValidator.ts
1
import { isClassStatement, isCommentStatement, isConstStatement, isDottedGetExpression, isEnumStatement, isForEachStatement, isForStatement, isFunctionStatement, isImportStatement, isInterfaceStatement, isLibraryStatement, isLiteralExpression, isNamespacedVariableNameExpression, 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 } from '../../parser/AstNode';
8
import type { LiteralExpression } from '../../parser/Expression';
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 util from '../../util';
1✔
13

14
export class BrsFileValidator {
1✔
15
    constructor(
16
        public event: OnFileValidateEvent<BrsFile>
554✔
17
    ) {
18
    }
19

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

30
    /**
31
     * Set the parent node on a given AstNode. This handles some edge cases where not every expression is iterated normally,
32
     * so it will also reach into nested objects to set their parent values as well
33
     */
34
    private setParent(node: AstNode, parent: AstNode) {
35
        const pairs = [[node, parent]];
6,191✔
36
        while (pairs.length > 0) {
6,191✔
37
            const [childNode, parentNode] = pairs.pop();
6,854✔
38
            //skip this entry if there's already a parent
39
            if (childNode?.parent) {
6,854!
40
                continue;
555✔
41
            }
42
            if (isDottedGetExpression(childNode)) {
6,299✔
43
                if (!childNode.obj.parent) {
335✔
44
                    pairs.push([childNode.obj, childNode]);
334✔
45
                }
46
            } else if (isNamespaceStatement(childNode)) {
5,964✔
47
                //namespace names shouldn't be walked, but it needs its parent assigned
48
                pairs.push([childNode.nameExpression, childNode]);
98✔
49
            } else if (isClassStatement(childNode)) {
5,866✔
50
                //class extends names don't get walked, but it needs its parent
51
                if (childNode.parentClassName) {
150✔
52
                    pairs.push([childNode.parentClassName, childNode]);
52✔
53
                }
54
            } else if (isInterfaceStatement(childNode)) {
5,716✔
55
                //class extends names don't get walked, but it needs its parent
56
                if (childNode.parentInterfaceName) {
12!
57
                    pairs.push([childNode.parentInterfaceName, childNode]);
×
58
                }
59
            } else if (isNamespacedVariableNameExpression(childNode)) {
5,704✔
60
                pairs.push([childNode.expression, childNode]);
179✔
61
            }
62
            childNode.parent = parentNode;
6,299✔
63
            //if the node has a symbol table, link it to its parent's symbol table
64
            if (childNode.symbolTable) {
6,299✔
65
                const parentSymbolTable = parentNode.getSymbolTable();
766✔
66
                if (parentSymbolTable) {
766!
67
                    childNode.symbolTable.pushParent(parentSymbolTable);
766✔
68
                }
69
            }
70
        }
71
    }
72

73
    /**
74
     * Walk the full AST
75
     */
76
    private walk() {
77
        const visitor = createVisitor({
554✔
78
            MethodStatement: (node) => {
79
                //add the `super` symbol to class methods
80
                node.func.body.symbolTable.addSymbol('super', undefined, DynamicType.instance);
85✔
81
            },
82
            EnumStatement: (node) => {
83
                this.validateEnumDeclaration(node);
47✔
84

85
                //register this enum declaration
86
                node.parent.getSymbolTable()?.addSymbol(node.tokens.name.text, node.tokens.name.range, DynamicType.instance);
47!
87
            },
88
            ClassStatement: (node) => {
89
                //register this class
90
                node.parent.getSymbolTable()?.addSymbol(node.name.text, node.name.range, DynamicType.instance);
150!
91
            },
92
            AssignmentStatement: (node) => {
93
                //register this variable
94
                node.parent.getSymbolTable()?.addSymbol(node.name.text, node.name.range, DynamicType.instance);
287!
95
            },
96
            FunctionParameterExpression: (node) => {
97
                node.getSymbolTable()?.addSymbol(node.name.text, node.name.range, node.type);
171!
98
            },
99
            ForEachStatement: (node) => {
100
                //register the for loop variable
101
                node.parent.getSymbolTable()?.addSymbol(node.item.text, node.item.range, DynamicType.instance);
7!
102
            },
103
            NamespaceStatement: (node) => {
104
                node.parent.getSymbolTable().addSymbol(
98✔
105
                    node.name.split('.')[0],
106
                    node.nameExpression.range,
107
                    DynamicType.instance
108
                );
109
            },
110
            FunctionStatement: (node) => {
111
                if (node.name?.text) {
499✔
112
                    node.parent.getSymbolTable().addSymbol(
498✔
113
                        node.name.text,
114
                        node.name.range,
115
                        DynamicType.instance
116
                    );
117
                }
118

119
                const namespace = node.findAncestor(isNamespaceStatement);
499✔
120
                //this function is declared inside a namespace
121
                if (namespace) {
499✔
122
                    //add the transpiled name for namespaced functions to the root symbol table
123
                    const transpiledNamespaceFunctionName = node.getName(ParseMode.BrightScript);
51✔
124
                    const funcType = node.func.getFunctionType();
51✔
125
                    funcType.setName(transpiledNamespaceFunctionName);
51✔
126

127
                    this.event.file.parser.ast.symbolTable.addSymbol(
51✔
128
                        transpiledNamespaceFunctionName,
129
                        node.name.range,
130
                        funcType
131
                    );
132
                }
133
            },
134
            FunctionExpression: (node) => {
135
                if (!node.body.symbolTable.hasSymbol('m')) {
591!
136
                    node.body.symbolTable.addSymbol('m', undefined, DynamicType.instance);
591✔
137
                }
138
            },
139
            ConstStatement: (node) => {
140
                node.parent.getSymbolTable().addSymbol(node.tokens.name.text, node.tokens.name.range, DynamicType.instance);
22✔
141
            },
142
            CatchStatement: (node) => {
143
                node.parent.getSymbolTable().addSymbol(node.exceptionVariable.text, node.exceptionVariable.range, DynamicType.instance);
3✔
144
            },
145
            DimStatement: (node) => {
146
                if (node.identifier) {
17!
147
                    node.parent.getSymbolTable().addSymbol(node.identifier.text, node.identifier.range, DynamicType.instance);
17✔
148
                }
149
            },
150
            ContinueStatement: (node) => {
151
                this.validateContinueStatement(node);
6✔
152
            }
153
        });
154

155
        this.event.file.ast.walk((node, parent) => {
554✔
156
            // link every child with its parent
157
            this.setParent(node, parent);
6,191✔
158
            visitor(node, parent);
6,191✔
159
        }, {
160
            walkMode: WalkMode.visitAllRecursive
161
        });
162
    }
163

164
    private validateEnumDeclaration(stmt: EnumStatement) {
165
        const members = stmt.getMembers();
47✔
166
        //the enum data type is based on the first member value
167
        const enumValueKind = (members.find(x => x.value)?.value as LiteralExpression)?.token?.kind ?? TokenKind.IntegerLiteral;
62✔
168
        const memberNames = new Set<string>();
47✔
169
        for (const member of members) {
47✔
170
            const memberNameLower = member.name?.toLowerCase();
86!
171

172
            /**
173
             * flag duplicate member names
174
             */
175
            if (memberNames.has(memberNameLower)) {
86✔
176
                this.event.file.addDiagnostic({
1✔
177
                    ...DiagnosticMessages.duplicateIdentifier(member.name),
178
                    range: member.range
179
                });
180
            } else {
181
                memberNames.add(memberNameLower);
85✔
182
            }
183

184
            //Enforce all member values are the same type
185
            this.validateEnumValueTypes(member, enumValueKind);
86✔
186
        }
187
    }
188

189
    private validateEnumValueTypes(member: EnumMemberStatement, enumValueKind: TokenKind) {
190
        let memberValueKind: TokenKind;
191
        let memberValue: Expression;
192
        if (isUnaryExpression(member.value)) {
86✔
193
            memberValueKind = (member.value?.right as LiteralExpression)?.token?.kind;
2!
194
            memberValue = member.value?.right;
2!
195
        } else {
196
            memberValueKind = (member.value as LiteralExpression)?.token?.kind;
84✔
197
            memberValue = member.value;
84✔
198
        }
199
        const range = (memberValue ?? member)?.range;
86!
200
        if (
86✔
201
            //is integer enum, has value, that value type is not integer
202
            (enumValueKind === TokenKind.IntegerLiteral && memberValueKind && memberValueKind !== enumValueKind) ||
280✔
203
            //has value, that value is not a literal
204
            (memberValue && !isLiteralExpression(memberValue))
205
        ) {
206
            this.event.file.addDiagnostic({
5✔
207
                ...DiagnosticMessages.enumValueMustBeType(
208
                    enumValueKind.replace(/literal$/i, '').toLowerCase()
209
                ),
210
                range: range
211
            });
212
        }
213

214
        //is non integer value
215
        if (enumValueKind !== TokenKind.IntegerLiteral) {
86✔
216
            //default value present
217
            if (memberValueKind) {
33✔
218
                //member value is same as enum
219
                if (memberValueKind !== enumValueKind) {
31✔
220
                    this.event.file.addDiagnostic({
1✔
221
                        ...DiagnosticMessages.enumValueMustBeType(
222
                            enumValueKind.replace(/literal$/i, '').toLowerCase()
223
                        ),
224
                        range: range
225
                    });
226
                }
227

228
                //default value missing
229
            } else {
230
                this.event.file.addDiagnostic({
2✔
231
                    file: this.event.file,
232
                    ...DiagnosticMessages.enumValueIsRequired(
233
                        enumValueKind.replace(/literal$/i, '').toLowerCase()
234
                    ),
235
                    range: range
236
                });
237
            }
238
        }
239
    }
240

241
    /**
242
     * Find statements defined at the top level (or inside a namespace body) that are not allowed to be there
243
     */
244
    private flagTopLevelStatements() {
245
        const statements = [...this.event.file.ast.statements];
552✔
246
        while (statements.length > 0) {
552✔
247
            const statement = statements.pop();
859✔
248
            if (isNamespaceStatement(statement)) {
859✔
249
                statements.push(...statement.body.statements);
97✔
250
            } else {
251
                //only allow these statement types
252
                if (
762✔
253
                    !isFunctionStatement(statement) &&
1,388✔
254
                    !isClassStatement(statement) &&
255
                    !isEnumStatement(statement) &&
256
                    !isInterfaceStatement(statement) &&
257
                    !isCommentStatement(statement) &&
258
                    !isLibraryStatement(statement) &&
259
                    !isImportStatement(statement) &&
260
                    !isConstStatement(statement)
261
                ) {
262
                    this.event.file.addDiagnostic({
6✔
263
                        ...DiagnosticMessages.unexpectedStatementOutsideFunction(),
264
                        range: statement.range
265
                    });
266
                }
267
            }
268
        }
269
    }
270

271
    private validateImportStatements() {
272
        let topOfFileIncludeStatements = [] as Array<LibraryStatement | ImportStatement>;
551✔
273
        for (let stmt of this.event.file.parser.ast.statements) {
551✔
274
            //skip comments
275
            if (isCommentStatement(stmt)) {
538✔
276
                continue;
4✔
277
            }
278
            //if we found a non-library statement, this statement is not at the top of the file
279
            if (isLibraryStatement(stmt) || isImportStatement(stmt)) {
534✔
280
                topOfFileIncludeStatements.push(stmt);
19✔
281
            } else {
282
                //break out of the loop, we found all of our library statements
283
                break;
515✔
284
            }
285
        }
286

287
        let statements = [
551✔
288
            // eslint-disable-next-line @typescript-eslint/dot-notation
289
            ...this.event.file['_parser'].references.libraryStatements,
290
            // eslint-disable-next-line @typescript-eslint/dot-notation
291
            ...this.event.file['_parser'].references.importStatements
292
        ];
293
        for (let result of statements) {
551✔
294
            //if this statement is not one of the top-of-file statements,
295
            //then add a diagnostic explaining that it is invalid
296
            if (!topOfFileIncludeStatements.includes(result)) {
22✔
297
                if (isLibraryStatement(result)) {
3✔
298
                    this.event.file.diagnostics.push({
2✔
299
                        ...DiagnosticMessages.libraryStatementMustBeDeclaredAtTopOfFile(),
300
                        range: result.range,
301
                        file: this.event.file
302
                    });
303
                } else if (isImportStatement(result)) {
1!
304
                    this.event.file.diagnostics.push({
1✔
305
                        ...DiagnosticMessages.importStatementMustBeDeclaredAtTopOfFile(),
306
                        range: result.range,
307
                        file: this.event.file
308
                    });
309
                }
310
            }
311
        }
312
    }
313

314
    private validateContinueStatement(statement: ContinueStatement) {
315
        const validateLoopTypeMatch = (loopType: TokenKind) => {
6✔
316
            //coerce ForEach to For
317
            loopType = loopType === TokenKind.ForEach ? TokenKind.For : loopType;
5✔
318

319
            if (loopType?.toLowerCase() !== statement.tokens.loopType.text?.toLowerCase()) {
5!
320
                this.event.file.addDiagnostic({
3✔
321
                    range: statement.tokens.loopType.range,
322
                    ...DiagnosticMessages.expectedToken(loopType)
323
                });
324
            }
325
        };
326

327
        //find the parent loop statement
328
        const parent = statement.findAncestor<WhileStatement | ForStatement | ForEachStatement>((node) => {
6✔
329
            if (isWhileStatement(node)) {
14✔
330
                validateLoopTypeMatch(node.tokens.while.kind);
2✔
331
                return true;
2✔
332
            } else if (isForStatement(node)) {
12✔
333
                validateLoopTypeMatch(node.forToken.kind);
2✔
334
                return true;
2✔
335
            } else if (isForEachStatement(node)) {
10✔
336
                validateLoopTypeMatch(node.tokens.forEach.kind);
1✔
337
                return true;
1✔
338
            }
339
        });
340
        //flag continue statements found outside of a loop
341
        if (!parent) {
6✔
342
            this.event.file.addDiagnostic({
1✔
343
                range: statement.range,
344
                ...DiagnosticMessages.illegalContinueStatement()
345
            });
346
        }
347
    }
348
}
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