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

rokucommunity / brighterscript / #12854

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

push

web-flow
Merge 7c29dfd7b into 5f3ffa3fa

10816 of 13510 branches covered (80.06%)

Branch coverage included in aggregate %.

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

318 existing lines in 19 files now uncovered.

12279 of 13462 relevant lines covered (91.21%)

26654.75 hits per line

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

92.71
/src/validators/ClassValidator.ts
1
import type { Scope } from '../Scope';
2
import { DiagnosticMessages } from '../DiagnosticMessages';
1✔
3
import type { CallExpression } from '../parser/Expression';
4
import { ParseMode } from '../parser/Parser';
1✔
5
import type { ClassStatement, MethodStatement, NamespaceStatement } from '../parser/Statement';
6
import { CancellationTokenSource } from 'vscode-languageserver';
1✔
7
import { isCallExpression, isFieldStatement, isMethodStatement, isNamespaceStatement } from '../astUtils/reflection';
1✔
8
import type { BsDiagnostic } from '../interfaces';
9
import { createVisitor, WalkMode } from '../astUtils/visitors';
1✔
10
import type { BrsFile } from '../files/BrsFile';
11
import { TokenKind } from '../lexer/TokenKind';
1✔
12
import { DynamicType } from '../types/DynamicType';
1✔
13
import type { BscType } from '../types/BscType';
14
import { SymbolTypeFlag } from '../SymbolTypeFlag';
1✔
15
import type { BscFile } from '../files/BscFile';
16

17
export class BsClassValidator {
1✔
18
    private scope: Scope;
19
    public diagnostics: BsDiagnostic[];
20
    /**
21
     * The key is the namespace-prefixed class name. (i.e. `NameA.NameB.SomeClass` or `CoolClass`)
22
     */
23
    private classes: Map<string, AugmentedClassStatement> = new Map();
1,541✔
24

25
    public constructor(scope: Scope) {
26
        this.scope = scope;
1,541✔
27
        this.diagnostics = [];
1,541✔
28
    }
29

30
    public validate() {
31
        this.findClasses();
1,541✔
32
        this.linkClassesWithParents();
1,541✔
33
        this.detectCircularReferences();
1,541✔
34
        this.validateMemberCollisions();
1,541✔
35
        this.verifyChildConstructor();
1,541✔
36

37
        this.cleanUp();
1,541✔
38
    }
39

40
    private verifyChildConstructor() {
41
        for (const [, classStatement] of this.classes) {
1,541✔
42
            const newMethod = classStatement.memberMap.new as MethodStatement;
378✔
43

44
            if (
378✔
45
                //this class has a "new method"
46
                newMethod &&
440✔
47
                //this class has a parent class
48
                classStatement.parentClass
49
            ) {
50
                //prevent use of `m.` anywhere before the `super()` call
51
                const cancellationToken = new CancellationTokenSource();
22✔
52
                let superCall: CallExpression | undefined;
53
                newMethod.func.body.walk(createVisitor({
22✔
54
                    VariableExpression: (expression, parent) => {
55
                        const expressionNameLower = expression?.tokens.name?.text.toLowerCase();
19!
56
                        if (expressionNameLower === 'm') {
19✔
57
                            this.diagnostics.push({
3✔
58
                                ...DiagnosticMessages.classConstructorIllegalUseOfMBeforeSuperCall(),
59
                                file: classStatement.file,
60
                                range: expression.location?.range
9!
61
                            });
62
                        }
63
                        if (isCallExpression(parent) && expressionNameLower === 'super') {
19✔
64
                            superCall = parent;
15✔
65
                            //stop walking
66
                            cancellationToken.cancel();
15✔
67
                        }
68
                    }
69
                }), {
70
                    walkMode: WalkMode.visitAll,
71
                    cancel: cancellationToken.token
72
                });
73

74
                //every child class constructor must include a call to `super()` (except for typedef files)
75
                if (!superCall && !(classStatement.file as BrsFile).isTypedef) {
22✔
76
                    this.diagnostics.push({
6✔
77
                        ...DiagnosticMessages.classConstructorMissingSuperCall(),
78
                        file: classStatement.file,
79
                        range: newMethod.location?.range
18!
80
                    });
81
                }
82
            }
83
        }
84
    }
85

86
    private detectCircularReferences() {
87
        for (let [, cls] of this.classes) {
1,541✔
88
            const names = new Map<string, string>();
378✔
89
            do {
378✔
90
                const className = cls.getName(ParseMode.BrighterScript);
481✔
91
                if (!className) {
481!
UNCOV
92
                    break;
×
93
                }
94
                const lowerClassName = className.toLowerCase();
481✔
95
                //if we've already seen this class name before, then we have a circular dependency
96
                if (lowerClassName && names.has(lowerClassName)) {
481✔
97
                    this.diagnostics.push({
5✔
98
                        ...DiagnosticMessages.circularReferenceDetected(
99
                            Array.from(names.values()).concat(className), this.scope.name),
100
                        file: cls.file,
101
                        range: cls.tokens.name.location?.range
15!
102
                    });
103
                    break;
5✔
104
                }
105
                names.set(lowerClassName, className);
476✔
106

107
                if (!cls.parentClass) {
476✔
108
                    break;
373✔
109
                }
110

111
                cls = cls.parentClass;
103✔
112
            } while (cls);
113
        }
114
    }
115

116
    private validateMemberCollisions() {
117
        for (const [, classStatement] of this.classes) {
1,541✔
118
            let methods = {};
378✔
119
            let fields = {};
378✔
120

121
            for (let statement of classStatement.body) {
378✔
122
                if (isMethodStatement(statement) || isFieldStatement(statement)) {
402!
123
                    let member = statement;
402✔
124
                    let memberName = member.tokens.name;
402✔
125

126
                    if (!memberName) {
402!
UNCOV
127
                        continue;
×
128
                    }
129

130
                    let lowerMemberName = memberName.text.toLowerCase();
402✔
131

132
                    //catch duplicate member names on same class
133
                    if (methods[lowerMemberName] || fields[lowerMemberName]) {
402✔
134
                        this.diagnostics.push({
4✔
135
                            ...DiagnosticMessages.duplicateIdentifier(memberName.text),
136
                            file: classStatement.file,
137
                            range: memberName.location?.range
12!
138
                        });
139
                    }
140

141
                    let memberType = isFieldStatement(member) ? 'field' : 'method';
402✔
142
                    let ancestorAndMember = this.getAncestorMember(classStatement, lowerMemberName);
402✔
143
                    if (ancestorAndMember) {
402✔
144
                        let ancestorMemberKind = isFieldStatement(ancestorAndMember.member) ? 'field' : 'method';
48✔
145

146
                        //mismatched member type (field/method in child, opposite in ancestor)
147
                        if (memberType !== ancestorMemberKind) {
48✔
148
                            this.diagnostics.push({
3✔
149
                                ...DiagnosticMessages.classChildMemberDifferentMemberTypeThanAncestor(
150
                                    memberType,
151
                                    ancestorMemberKind,
152
                                    ancestorAndMember.classStatement.getName(ParseMode.BrighterScript)
153
                                ),
154
                                file: classStatement.file,
155
                                range: member.location?.range
9!
156
                            });
157
                        }
158

159
                        //child field has same name as parent
160
                        if (isFieldStatement(member)) {
48✔
161
                            let ancestorMemberType: BscType = new DynamicType();
14✔
162
                            if (isFieldStatement(ancestorAndMember.member)) {
14✔
163
                                ancestorMemberType = ancestorAndMember.member.getType({ flags: SymbolTypeFlag.typetime });
13✔
164
                            } else if (isMethodStatement(ancestorAndMember.member)) {
1!
165
                                ancestorMemberType = ancestorAndMember.member.func.getType({ flags: SymbolTypeFlag.typetime });
1✔
166
                            }
167
                            const childFieldType = member.getType({ flags: SymbolTypeFlag.typetime });
14✔
168
                            if (childFieldType && !ancestorMemberType.isTypeCompatible(childFieldType)) {
14✔
169
                                //flag incompatible child field type to ancestor field type
170
                                this.diagnostics.push({
3✔
171
                                    ...DiagnosticMessages.childFieldTypeNotAssignableToBaseProperty(
172
                                        classStatement.getName(ParseMode.BrighterScript) ?? '',
9!
173
                                        ancestorAndMember.classStatement.getName(ParseMode.BrighterScript),
174
                                        memberName.text,
175
                                        childFieldType.toString(),
176
                                        ancestorMemberType.toString()
177
                                    ),
178
                                    file: classStatement.file,
179
                                    range: member.location?.range
9!
180
                                });
181
                            }
182
                        }
183

184
                        //child method missing the override keyword
185
                        if (
48✔
186
                            //is a method
187
                            isMethodStatement(member) &&
99✔
188
                            //does not have an override keyword
189
                            !member.tokens.override &&
190
                            //is not the constructur function
191
                            member.tokens.name.text.toLowerCase() !== 'new'
192
                        ) {
193
                            this.diagnostics.push({
2✔
194
                                ...DiagnosticMessages.missingOverrideKeyword(
195
                                    ancestorAndMember.classStatement.getName(ParseMode.BrighterScript)
196
                                ),
197
                                file: classStatement.file,
198
                                range: member.location?.range
6!
199
                            });
200
                        }
201

202
                        //child member has different visiblity
203
                        if (
48✔
204
                            //is a method
205
                            isMethodStatement(member) &&
82✔
206
                            (member.accessModifier?.kind ?? TokenKind.Public) !== (ancestorAndMember.member.accessModifier?.kind ?? TokenKind.Public)
408✔
207
                        ) {
208
                            this.diagnostics.push({
3✔
209
                                ...DiagnosticMessages.mismatchedOverriddenMemberVisibility(
210
                                    classStatement.tokens.name.text,
211
                                    ancestorAndMember.member.tokens.name?.text,
9!
212
                                    member.accessModifier?.text ?? 'public',
18✔
213
                                    ancestorAndMember.member.accessModifier?.text || 'public',
13✔
214
                                    ancestorAndMember.classStatement.getName(ParseMode.BrighterScript)
215
                                ),
216
                                file: classStatement.file,
217
                                range: member.location?.range
9!
218
                            });
219
                        }
220
                    }
221

222
                    if (isMethodStatement(member)) {
402✔
223
                        methods[lowerMemberName] = member;
213✔
224

225
                    } else if (isFieldStatement(member)) {
189!
226
                        fields[lowerMemberName] = member;
189✔
227
                    }
228
                }
229
            }
230
        }
231
    }
232

233
    /**
234
     * Get the closest member with the specified name (case-insensitive)
235
     */
236
    getAncestorMember(classStatement, memberName) {
237
        let lowerMemberName = memberName.toLowerCase();
402✔
238
        let ancestor = classStatement.parentClass;
402✔
239
        while (ancestor) {
402✔
240
            let member = ancestor.memberMap[lowerMemberName];
96✔
241
            if (member) {
96✔
242
                return {
48✔
243
                    member: member,
244
                    classStatement: ancestor
245
                };
246
            }
247
            ancestor = ancestor.parentClass !== ancestor ? ancestor.parentClass : null;
48!
248
        }
249
    }
250

251
    private cleanUp() {
252
        //unlink all classes from their parents so it doesn't mess up the next scope
253
        for (const [, classStatement] of this.classes) {
1,541✔
254
            delete classStatement.parentClass;
378✔
255
            delete (classStatement as any).file;
378✔
256
        }
257
    }
258

259
    private findClasses() {
260
        this.classes = new Map();
1,541✔
261
        this.scope.enumerateBrsFiles((file) => {
1,541✔
262

263
            // eslint-disable-next-line @typescript-eslint/dot-notation
264
            for (let x of file['_cachedLookups'].classStatements ?? []) {
1,911!
265
                let classStatement = x as AugmentedClassStatement;
381✔
266
                let name = classStatement.getName(ParseMode.BrighterScript);
381✔
267
                //skip this class if it doesn't have a name
268
                if (!name) {
381✔
269
                    continue;
1✔
270
                }
271
                let lowerName = name.toLowerCase();
380✔
272
                //see if this class was already defined
273
                let alreadyDefinedClass = this.classes.get(lowerName);
380✔
274

275
                //if we don't already have this class, register it
276
                if (!alreadyDefinedClass) {
380✔
277
                    this.classes.set(lowerName, classStatement);
378✔
278
                    classStatement.file = file;
378✔
279
                }
280
            }
281
        });
282
    }
283

284
    private linkClassesWithParents() {
285
        //link all classes with their parents
286
        for (const [, classStatement] of this.classes) {
1,541✔
287
            let parentClassName = classStatement.parentClassName?.getName();
378✔
288
            if (parentClassName) {
378✔
289
                let relativeName: string;
290
                let absoluteName: string;
291

292
                //if the parent class name was namespaced in the declaration of this class,
293
                //compute the relative name of the parent class and the absolute name of the parent class
294
                if (parentClassName.indexOf('.') > 0) {
91✔
295
                    absoluteName = parentClassName;
6✔
296
                    let parts = parentClassName.split('.');
6✔
297
                    relativeName = parts[parts.length - 1];
6✔
298

299
                    //the parent class name was NOT namespaced.
300
                    //compute the relative name of the parent class and prepend the current class's namespace
301
                    //to the beginning of the parent class's name
302
                } else {
303
                    const namespace = classStatement.findAncestor<NamespaceStatement>(isNamespaceStatement);
85✔
304
                    if (namespace) {
85✔
305
                        absoluteName = `${namespace.getName(ParseMode.BrighterScript)}.${parentClassName}`;
23✔
306
                    } else {
307
                        absoluteName = parentClassName;
62✔
308
                    }
309
                    relativeName = parentClassName;
85✔
310
                }
311

312
                let relativeParent = this.classes.get(relativeName.toLowerCase());
91✔
313
                let absoluteParent = this.classes.get(absoluteName.toLowerCase());
91✔
314

315
                let parentClass: AugmentedClassStatement | undefined;
316
                //if we found a relative parent class
317
                if (relativeParent) {
91✔
318
                    parentClass = relativeParent;
62✔
319

320
                    //we found an absolute parent class
321
                } else if (absoluteParent) {
29✔
322
                    parentClass = absoluteParent;
23✔
323

324
                } else {
325
                    //couldn't find the parent class (validated in ScopeValidator)
326
                }
327
                classStatement.parentClass = parentClass;
91✔
328
            }
329
        }
330
    }
331
}
332

333
type AugmentedClassStatement = ClassStatement & {
334
    file: BscFile;
335
    parentClass: AugmentedClassStatement | undefined;
336
};
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