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

rokucommunity / brighterscript / #13108

30 Sep 2024 02:32PM UTC coverage: 86.842% (-2.1%) from 88.98%
#13108

push

web-flow
Merge 9cec80aa6 into 356b8f6b8

11525 of 14034 branches covered (82.12%)

Branch coverage included in aggregate %.

12691 of 13851 relevant lines covered (91.63%)

29449.4 hits per line

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

94.92
/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,598✔
24

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

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

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

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

44
            if (
390✔
45
                //this class has a "new method"
46
                newMethod &&
455✔
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();
23✔
52
                let superCall: CallExpression | undefined;
53
                newMethod.func.body.walk(createVisitor({
23✔
54
                    VariableExpression: (expression, parent) => {
55
                        const expressionNameLower = expression?.tokens.name?.text.toLowerCase();
20!
56
                        if (expressionNameLower === 'm') {
20✔
57
                            this.diagnostics.push({
3✔
58
                                ...DiagnosticMessages.classConstructorIllegalUseOfMBeforeSuperCall(),
59
                                location: expression.location
60
                            });
61
                        }
62
                        if (isCallExpression(parent) && expressionNameLower === 'super') {
20✔
63
                            superCall = parent;
16✔
64
                            //stop walking
65
                            cancellationToken.cancel();
16✔
66
                        }
67
                    }
68
                }), {
69
                    walkMode: WalkMode.visitAll,
70
                    cancel: cancellationToken.token
71
                });
72

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

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

104
                if (!cls.parentClass) {
489✔
105
                    break;
385✔
106
                }
107

108
                cls = cls.parentClass;
104✔
109
            } while (cls);
110
        }
111
    }
112

113
    private validateMemberCollisions() {
114
        for (const [, classStatement] of this.classes) {
1,598✔
115
            let methods = {};
390✔
116
            let fields = {};
390✔
117

118
            for (let statement of classStatement.body) {
390✔
119
                if (isMethodStatement(statement) || isFieldStatement(statement)) {
416!
120
                    let member = statement;
416✔
121
                    let memberName = member.tokens.name;
416✔
122

123
                    if (!memberName) {
416!
124
                        continue;
×
125
                    }
126

127
                    let lowerMemberName = memberName.text.toLowerCase();
416✔
128

129
                    //catch duplicate member names on same class
130
                    if (methods[lowerMemberName] || fields[lowerMemberName]) {
416✔
131
                        this.diagnostics.push({
4✔
132
                            ...DiagnosticMessages.duplicateIdentifier(memberName.text),
133
                            location: memberName.location
134
                        });
135
                    }
136

137
                    let memberType = isFieldStatement(member) ? 'field' : 'method';
416✔
138
                    let ancestorAndMember = this.getAncestorMember(classStatement, lowerMemberName);
416✔
139
                    if (ancestorAndMember) {
416✔
140
                        let ancestorMemberKind = isFieldStatement(ancestorAndMember.member) ? 'field' : 'method';
49✔
141

142
                        //mismatched member type (field/method in child, opposite in ancestor)
143
                        if (memberType !== ancestorMemberKind) {
49✔
144
                            this.diagnostics.push({
3✔
145
                                ...DiagnosticMessages.classChildMemberDifferentMemberTypeThanAncestor(
146
                                    memberType,
147
                                    ancestorMemberKind,
148
                                    ancestorAndMember.classStatement.getName(ParseMode.BrighterScript)
149
                                ),
150
                                location: member.location
151
                            });
152
                        }
153

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

178
                        //child method missing the override keyword
179
                        if (
49✔
180
                            //is a method
181
                            isMethodStatement(member) &&
102✔
182
                            //does not have an override keyword
183
                            !member.tokens.override &&
184
                            //is not the constructur function
185
                            member.tokens.name.text.toLowerCase() !== 'new'
186
                        ) {
187
                            this.diagnostics.push({
2✔
188
                                ...DiagnosticMessages.missingOverrideKeyword(
189
                                    ancestorAndMember.classStatement.getName(ParseMode.BrighterScript)
190
                                ),
191
                                location: member.location
192
                            });
193
                        }
194

195
                        //child member has different visiblity
196
                        if (
49✔
197
                            //is a method
198
                            isMethodStatement(member) &&
84✔
199
                            (member.accessModifier?.kind ?? TokenKind.Public) !== (ancestorAndMember.member.accessModifier?.kind ?? TokenKind.Public)
420✔
200
                        ) {
201
                            this.diagnostics.push({
3✔
202
                                ...DiagnosticMessages.mismatchedOverriddenMemberVisibility(
203
                                    classStatement.tokens.name.text,
204
                                    ancestorAndMember.member.tokens.name?.text,
9!
205
                                    member.accessModifier?.text ?? 'public',
18✔
206
                                    ancestorAndMember.member.accessModifier?.text || 'public',
13✔
207
                                    ancestorAndMember.classStatement.getName(ParseMode.BrighterScript)
208
                                ),
209
                                location: member.location
210
                            });
211
                        }
212
                    }
213

214
                    if (isMethodStatement(member)) {
416✔
215
                        methods[lowerMemberName] = member;
219✔
216

217
                    } else if (isFieldStatement(member)) {
197!
218
                        fields[lowerMemberName] = member;
197✔
219
                    }
220
                }
221
            }
222
        }
223
    }
224

225
    /**
226
     * Get the closest member with the specified name (case-insensitive)
227
     */
228
    getAncestorMember(classStatement, memberName) {
229
        let lowerMemberName = memberName.toLowerCase();
416✔
230
        let ancestor = classStatement.parentClass;
416✔
231
        while (ancestor) {
416✔
232
            let member = ancestor.memberMap[lowerMemberName];
97✔
233
            if (member) {
97✔
234
                return {
49✔
235
                    member: member,
236
                    classStatement: ancestor
237
                };
238
            }
239
            ancestor = ancestor.parentClass !== ancestor ? ancestor.parentClass : null;
48!
240
        }
241
    }
242

243
    private cleanUp() {
244
        //unlink all classes from their parents so it doesn't mess up the next scope
245
        for (const [, classStatement] of this.classes) {
1,598✔
246
            delete classStatement.parentClass;
390✔
247
            delete (classStatement as any).file;
390✔
248
        }
249
    }
250

251
    private findClasses() {
252
        this.classes = new Map();
1,598✔
253
        this.scope.enumerateBrsFiles((file) => {
1,598✔
254

255
            // eslint-disable-next-line @typescript-eslint/dot-notation
256
            for (let x of file['_cachedLookups'].classStatements ?? []) {
1,977!
257
                let classStatement = x as AugmentedClassStatement;
393✔
258
                let name = classStatement.getName(ParseMode.BrighterScript);
393✔
259
                //skip this class if it doesn't have a name
260
                if (!name) {
393✔
261
                    continue;
1✔
262
                }
263
                let lowerName = name.toLowerCase();
392✔
264
                //see if this class was already defined
265
                let alreadyDefinedClass = this.classes.get(lowerName);
392✔
266

267
                //if we don't already have this class, register it
268
                if (!alreadyDefinedClass) {
392✔
269
                    this.classes.set(lowerName, classStatement);
390✔
270
                    classStatement.file = file;
390✔
271
                }
272
            }
273
        });
274
    }
275

276
    private linkClassesWithParents() {
277
        //link all classes with their parents
278
        for (const [, classStatement] of this.classes) {
1,598✔
279
            let parentClassName = classStatement.parentClassName?.getName();
390✔
280
            if (parentClassName) {
390✔
281
                let relativeName: string;
282
                let absoluteName: string;
283

284
                //if the parent class name was namespaced in the declaration of this class,
285
                //compute the relative name of the parent class and the absolute name of the parent class
286
                if (parentClassName.indexOf('.') > 0) {
92✔
287
                    absoluteName = parentClassName;
6✔
288
                    let parts = parentClassName.split('.');
6✔
289
                    relativeName = parts[parts.length - 1];
6✔
290

291
                    //the parent class name was NOT namespaced.
292
                    //compute the relative name of the parent class and prepend the current class's namespace
293
                    //to the beginning of the parent class's name
294
                } else {
295
                    const namespace = classStatement.findAncestor<NamespaceStatement>(isNamespaceStatement);
86✔
296
                    if (namespace) {
86✔
297
                        absoluteName = `${namespace.getName(ParseMode.BrighterScript)}.${parentClassName}`;
24✔
298
                    } else {
299
                        absoluteName = parentClassName;
62✔
300
                    }
301
                    relativeName = parentClassName;
86✔
302
                }
303

304
                let relativeParent = this.classes.get(relativeName.toLowerCase());
92✔
305
                let absoluteParent = this.classes.get(absoluteName.toLowerCase());
92✔
306

307
                let parentClass: AugmentedClassStatement | undefined;
308
                //if we found a relative parent class
309
                if (relativeParent) {
92✔
310
                    parentClass = relativeParent;
62✔
311

312
                    //we found an absolute parent class
313
                } else if (absoluteParent) {
30✔
314
                    parentClass = absoluteParent;
24✔
315

316
                } else {
317
                    //couldn't find the parent class (validated in ScopeValidator)
318
                }
319
                classStatement.parentClass = parentClass;
92✔
320
            }
321
        }
322
    }
323
}
324

325
type AugmentedClassStatement = ClassStatement & {
326
    file: BscFile;
327
    parentClass: AugmentedClassStatement | undefined;
328
};
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

© 2025 Coveralls, Inc