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

rokucommunity / brighterscript / #15637

28 Apr 2026 03:19PM UTC coverage: 88.999% (-0.01%) from 89.009%
#15637

push

web-flow
Merge b388c7d11 into 9cb905446

8258 of 9776 branches covered (84.47%)

Branch coverage included in aggregate %.

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

12 existing lines in 2 files now uncovered.

10463 of 11259 relevant lines covered (92.93%)

2026.41 hits per line

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

92.11
/src/Scope.ts
1
import type { CompletionItem, Position, Range, Location } from 'vscode-languageserver';
2
import * as path from 'path';
1✔
3
import { CompletionItemKind } from 'vscode-languageserver';
1✔
4
import chalk from 'chalk';
1✔
5
import type { DiagnosticInfo } from './DiagnosticMessages';
6
import { DiagnosticMessages } from './DiagnosticMessages';
1✔
7
import type { CallableContainer, BsDiagnostic, FileReference, BscFile, CallableContainerMap, FileLink, Callable } from './interfaces';
8
import type { Program } from './Program';
9
import { BsClassValidator } from './validators/ClassValidator';
1✔
10
import type { NamespaceStatement, FunctionStatement, ClassStatement, EnumStatement, InterfaceStatement, EnumMemberStatement, ConstStatement, TypeStatement } from './parser/Statement';
11
import type { NewExpression } from './parser/Expression';
12
import { ParseMode } from './parser/Parser';
1✔
13
import { util } from './util';
1✔
14
import { globalCallableMap } from './globalCallables';
1✔
15
import { Cache } from './Cache';
1✔
16
import { URI } from 'vscode-uri';
1✔
17
import type { BrsFile } from './files/BrsFile';
18
import type { DependencyGraph, DependencyChangedEvent } from './DependencyGraph';
19
import { isBrsFile, isMethodStatement, isClassStatement, isConstStatement, isCustomType, isEnumStatement, isFunctionStatement, isFunctionType, isXmlFile, isNamespaceStatement, isEnumMemberStatement } from './astUtils/reflection';
1✔
20
import { SymbolTable } from './SymbolTable';
1✔
21
import type { Statement } from './parser/AstNode';
22
import { LogLevel } from './logging';
1✔
23

24
/**
25
 * A class to keep track of all declarations within a given scope (like source scope, component scope)
26
 */
27
export class Scope {
1✔
28
    constructor(
29
        public name: string,
2,952✔
30
        public program: Program,
2,952✔
31
        private _dependencyGraphKey?: string
2,952✔
32
    ) {
33
        this.isValidated = false;
2,952✔
34
        //used for improved logging performance
35
        this._debugLogComponentName = `Scope '${chalk.redBright(this.name)}'`;
2,952✔
36
    }
37

38
    /**
39
     * Indicates whether this scope needs to be validated.
40
     * Will be true when first constructed, or anytime one of its dependencies changes
41
     */
42
    public readonly isValidated: boolean;
43

44
    protected cache = new Cache();
2,952✔
45

46
    public get dependencyGraphKey() {
47
        return this._dependencyGraphKey;
3,622✔
48
    }
49

50
    /**
51
     * A dictionary of namespaces, indexed by the lower case full name of each namespace.
52
     * If a namespace is declared as "NameA.NameB.NameC", there will be 3 entries in this dictionary,
53
     * "namea", "namea.nameb", "namea.nameb.namec"
54
     */
55
    public get namespaceLookup() {
56
        return this.cache.getOrAdd('namespaceLookup', () => this.buildNamespaceLookup());
3,122✔
57
    }
58

59
    /**
60
     * Get a NamespaceContainer by its name, looking for a fully qualified version first, then global version next if not found
61
     */
62
    public getNamespace(name: string, containingNamespace?: string) {
63
        const nameLower = name?.toLowerCase();
2,132!
64
        const lookup = this.namespaceLookup;
2,132✔
65

66
        let ns: NamespaceContainer;
67
        if (containingNamespace) {
2,132✔
68
            ns = lookup.get(`${containingNamespace?.toLowerCase()}.${nameLower}`);
208!
69
        }
70
        //if we couldn't find the namespace by its full namespaced name, look for a global version
71
        if (!ns) {
2,132✔
72
            ns = lookup.get(nameLower);
2,117✔
73
        }
74
        return ns;
2,132✔
75
    }
76

77
    /**
78
     * Get the class with the specified name.
79
     * @param className - The class name, including the namespace of the class if possible
80
     * @param containingNamespace - The namespace used to resolve relative class names. (i.e. the namespace around the current statement trying to find a class)
81
     */
82
    public getClass(className: string, containingNamespace?: string): ClassStatement {
83
        return this.getClassFileLink(className, containingNamespace)?.item;
119,341✔
84
    }
85

86
    /**
87
     * Get the interface with the specified name.
88
     * @param ifaceName - The interface name, including the namespace of the interface if possible
89
     * @param containingNamespace - The namespace used to resolve relative interface names. (i.e. the namespace around the current statement trying to find a interface)
90
     */
91
    public getInterface(ifaceName: string, containingNamespace?: string): InterfaceStatement {
92
        return this.getInterfaceFileLink(ifaceName, containingNamespace)?.item;
32✔
93
    }
94

95
    /**
96
     * Get the enum with the specified name.
97
     * @param enumName - The enum name, including the namespace if possible
98
     * @param containingNamespace - The namespace used to resolve relative enum names. (i.e. the namespace around the current statement trying to find an enum)
99
     */
100
    public getEnum(enumName: string, containingNamespace?: string): EnumStatement {
101
        return this.getEnumFileLink(enumName, containingNamespace)?.item;
911✔
102
    }
103

104
    public getTypeStatement(typeName: string, containingNamespace?: string): Statement {
105
        return this.getTypeStatementFileLink(typeName, containingNamespace)?.item;
18✔
106
    }
107

108
    /**
109
     * Get a class and its containing file by the class name
110
     * @param className - The class name, including the namespace of the class if possible
111
     * @param containingNamespace - The namespace used to resolve relative class names. (i.e. the namespace around the current statement trying to find a class)
112
     */
113
    public getClassFileLink(className: string, containingNamespace?: string): FileLink<ClassStatement> {
114
        const lowerClassName = className?.toLowerCase();
119,599✔
115
        const classMap = this.getClassMap();
119,599✔
116

117
        let cls = classMap.get(
119,599✔
118
            util.getFullyQualifiedClassName(lowerClassName, containingNamespace?.toLowerCase())
358,797✔
119
        );
120
        //if we couldn't find the class by its full namespaced name, look for a global class with that name
121
        if (!cls) {
119,599✔
122
            cls = classMap.get(lowerClassName);
119,331✔
123
        }
124
        return cls;
119,599✔
125
    }
126

127

128
    /**
129
     * Get an interface and its containing file by the interface name
130
     * @param ifaceName - The interface name, including the namespace of the interface if possible
131
     * @param containingNamespace - The namespace used to resolve relative interface names. (i.e. the namespace around the current statement trying to find a interface)
132
     */
133
    public getInterfaceFileLink(ifaceName: string, containingNamespace?: string): FileLink<InterfaceStatement> {
134
        const lowerName = ifaceName?.toLowerCase();
32!
135
        const ifaceMap = this.getInterfaceMap();
32✔
136

137
        let iface = ifaceMap.get(
32✔
138
            util.getFullyQualifiedClassName(lowerName, containingNamespace?.toLowerCase())
96!
139
        );
140
        //if we couldn't find the iface by its full namespaced name, look for a global class with that name
141
        if (!iface) {
32✔
142
            iface = ifaceMap.get(lowerName);
25✔
143
        }
144
        return iface;
32✔
145
    }
146

147
    /**
148
     * Get an Enum and its containing file by the Enum name
149
     * @param enumName - The Enum name, including the namespace of the enum if possible
150
     * @param containingNamespace - The namespace used to resolve relative enum names. (i.e. the namespace around the current statement trying to find a enum)
151
     */
152
    public getEnumFileLink(enumName: string, containingNamespace?: string): FileLink<EnumStatement> {
153
        const lowerName = enumName?.toLowerCase();
1,400✔
154
        const enumMap = this.getEnumMap();
1,400✔
155

156
        let enumeration = enumMap.get(
1,400✔
157
            util.getFullyQualifiedClassName(lowerName, containingNamespace?.toLowerCase())
4,200✔
158
        );
159
        //if we couldn't find the enum by its full namespaced name, look for a global enum with that name
160
        if (!enumeration) {
1,400✔
161
            enumeration = enumMap.get(lowerName);
1,247✔
162
        }
163
        return enumeration;
1,400✔
164
    }
165

166
    /**
167
     * Get an Enum and its containing file by the Enum name
168
     * @param enumMemberName - The Enum name, including the namespace of the enum if possible
169
     * @param containingNamespace - The namespace used to resolve relative enum names. (i.e. the namespace around the current statement trying to find a enum)
170
     */
171
    public getEnumMemberFileLink(enumMemberName: string, containingNamespace?: string): FileLink<EnumMemberStatement> {
172
        let lowerNameParts = enumMemberName?.toLowerCase()?.split('.');
885✔
173
        let memberName = lowerNameParts?.splice(lowerNameParts.length - 1, 1)?.[0];
885✔
174
        let lowerName = lowerNameParts?.join('.').toLowerCase();
885✔
175
        const enumMap = this.getEnumMap();
885✔
176

177
        let enumeration = enumMap.get(
885✔
178
            util.getFullyQualifiedClassName(lowerName, containingNamespace?.toLowerCase())
2,655✔
179
        );
180
        //if we couldn't find the enum by its full namespaced name, look for a global enum with that name
181
        if (!enumeration) {
885✔
182
            enumeration = enumMap.get(lowerName);
702✔
183
        }
184
        if (enumeration) {
885✔
185
            let member = enumeration.item.findChild<EnumMemberStatement>((child) => isEnumMemberStatement(child) && child.name?.toLowerCase() === memberName);
376!
186
            return member ? { item: member, file: enumeration.file } : undefined;
185✔
187
        }
188
    }
189

190
    /**
191
     * Get a constant and its containing file by the constant name
192
     * @param constName - The constant name, including the namespace of the constant if possible
193
     * @param containingNamespace - The namespace used to resolve relative constant names. (i.e. the namespace around the current statement trying to find a constant)
194
     */
195
    public getConstFileLink(constName: string, containingNamespace?: string): FileLink<ConstStatement> {
196
        const lowerName = constName?.toLowerCase();
875✔
197
        const constMap = this.getConstMap();
875✔
198

199
        let result = constMap.get(
875✔
200
            util.getFullyQualifiedClassName(lowerName, containingNamespace?.toLowerCase())
2,625✔
201
        );
202
        //if we couldn't find the constant by its full namespaced name, look for a global constant with that name
203
        if (!result) {
875✔
204
            result = constMap.get(lowerName);
708✔
205
        }
206
        return result;
875✔
207
    }
208

209
    /**
210
     * Get an TypeStatement and its containing file by the type name
211
     * @param typeName - The TypeStatement name, including the namespace of the type if possible
212
     * @param containingNamespace - The namespace used to resolve relative type names. (i.e. the namespace around the current statement trying to find a type)
213
     */
214
    public getTypeStatementFileLink(typeName: string, containingNamespace?: string): FileLink<TypeStatement> {
215
        const lowerName = typeName?.toLowerCase();
18!
216
        const typeStatementMap = this.getTypeStatementMap();
18✔
217

218
        let typeStatement = typeStatementMap.get(
18✔
219
            util.getFullyQualifiedClassName(lowerName, containingNamespace?.toLowerCase())
54!
220
        );
221
        //if we couldn't find the enum by its full namespaced name, look for a global enum with that name
222
        if (!typeStatement) {
18✔
223
            typeStatement = typeStatementMap.get(lowerName);
10✔
224
        }
225
        return typeStatement;
18✔
226
    }
227

228
    /**
229
     * Get a map of all enums by their member name.
230
     * The keys are lower-case fully-qualified paths to the enum and its member. For example:
231
     * namespace.enum.value
232
     */
233
    public getEnumMemberMap() {
234
        return this.cache.getOrAdd('enumMemberMap', () => {
×
235
            const result = new Map<string, EnumMemberStatement>();
×
236
            for (const [key, eenum] of this.getEnumMap()) {
×
237
                for (const member of eenum.item.getMembers()) {
×
238
                    result.set(`${key}.${member.name.toLowerCase()}`, member);
×
239
                }
240
            }
241
            return result;
×
242
        });
243
    }
244

245

246
    /**
247
     * Tests if a class exists with the specified name
248
     * @param className - the all-lower-case namespace-included class name
249
     * @param namespaceName - The namespace used to resolve relative class names. (i.e. the namespace around the current statement trying to find a class)
250
     */
251
    public hasClass(className: string, namespaceName?: string): boolean {
252
        return !!this.getClass(className, namespaceName);
119,286✔
253
    }
254

255
    /**
256
     * Tests if an interface exists with the specified name
257
     * @param ifaceName - the all-lower-case namespace-included interface name
258
     * @param namespaceName - the current namespace name
259
     */
260
    public hasInterface(ifaceName: string, namespaceName?: string): boolean {
261
        return !!this.getInterface(ifaceName, namespaceName);
32✔
262
    }
263

264
    /**
265
     * Tests if an enum exists with the specified name
266
     * @param enumName - the all-lower-case namespace-included enum name
267
     * @param namespaceName - the current namespace name
268
     */
269
    public hasEnum(enumName: string, namespaceName?: string): boolean {
270
        return !!this.getEnum(enumName, namespaceName);
25✔
271
    }
272

273
    public hasTypeStatementType(typeName: string, namespaceName?: string): boolean {
274
        return !!this.getTypeStatement(typeName, namespaceName);
18✔
275
    }
276

277
    /**
278
     * A dictionary of all classes in this scope. This includes namespaced classes always with their full name.
279
     * The key is stored in lower case
280
     */
281
    public getClassMap(): Map<string, FileLink<ClassStatement>> {
282
        return this.cache.getOrAdd('classMap', () => {
122,340✔
283
            const map = new Map<string, FileLink<ClassStatement>>();
2,470✔
284
            this.enumerateBrsFiles((file) => {
2,470✔
285
                if (isBrsFile(file)) {
2,606!
286
                    for (let cls of file.parser.references.classStatements) {
2,606✔
287
                        const lowerClassName = cls.getName(ParseMode.BrighterScript)?.toLowerCase();
218✔
288
                        //only track classes with a defined name (i.e. exclude nameless malformed classes)
289
                        if (lowerClassName) {
218✔
290
                            map.set(lowerClassName, { item: cls, file: file });
217✔
291
                        }
292
                    }
293
                }
294
            });
295
            return map;
2,470✔
296
        });
297
    }
298

299
    /**
300
     * A dictionary of all Interfaces in this scope. This includes namespaced Interfaces always with their full name.
301
     * The key is stored in lower case
302
     */
303
    public getInterfaceMap(): Map<string, FileLink<InterfaceStatement>> {
304
        return this.cache.getOrAdd('interfaceMap', () => {
32✔
305
            const map = new Map<string, FileLink<InterfaceStatement>>();
20✔
306
            this.enumerateBrsFiles((file) => {
20✔
307
                if (isBrsFile(file)) {
20!
308
                    for (let iface of file.parser.references.interfaceStatements) {
20✔
309
                        const lowerIfaceName = iface.getName(ParseMode.BrighterScript)?.toLowerCase();
5!
310
                        //only track classes with a defined name (i.e. exclude nameless malformed classes)
311
                        if (lowerIfaceName) {
5!
312
                            map.set(lowerIfaceName, { item: iface, file: file });
5✔
313
                        }
314
                    }
315
                }
316
            });
317
            return map;
20✔
318
        });
319
    }
320

321
    /**
322
     * A dictionary of all enums in this scope. This includes namespaced enums always with their full name.
323
     * The key is stored in lower case
324
     */
325
    public getEnumMap(): Map<string, FileLink<EnumStatement>> {
326
        return this.cache.getOrAdd('enumMap', () => {
2,571✔
327
            const map = new Map<string, FileLink<EnumStatement>>();
432✔
328
            this.enumerateBrsFiles((file) => {
432✔
329
                for (let enumStmt of file.parser.references.enumStatements) {
499✔
330
                    const lowerEnumName = enumStmt.fullName.toLowerCase();
82✔
331
                    //only track enums with a defined name (i.e. exclude nameless malformed enums)
332
                    if (lowerEnumName) {
82!
333
                        map.set(lowerEnumName, { item: enumStmt, file: file });
82✔
334
                    }
335
                }
336
            });
337
            return map;
432✔
338
        });
339
    }
340

341
    /**
342
     * A dictionary of all constants in this scope. This includes namespaced constants always with their full name.
343
     * The key is stored in lower case
344
     */
345
    public getConstMap(): Map<string, FileLink<ConstStatement>> {
346
        return this.cache.getOrAdd('constMap', () => {
3,613✔
347
            const map = new Map<string, FileLink<ConstStatement>>();
2,587✔
348
            this.enumerateBrsFiles((file) => {
2,587✔
349
                for (let stmt of file.parser.references.constStatements) {
2,633✔
350
                    const lowerEnumName = stmt.fullName.toLowerCase();
98✔
351
                    //only track enums with a defined name (i.e. exclude nameless malformed enums)
352
                    if (lowerEnumName) {
98!
353
                        map.set(lowerEnumName, { item: stmt, file: file });
98✔
354
                    }
355
                }
356
            });
357
            return map;
2,587✔
358
        });
359
    }
360

361
    /**
362
     * A dictionary of all TypeStatements in this scope. This includes namespaced types always with their full name.
363
     * The key is stored in lower case
364
     */
365
    public getTypeStatementMap(): Map<string, FileLink<TypeStatement>> {
366
        return this.cache.getOrAdd('typeStatementMap', () => {
18✔
367
            const map = new Map<string, FileLink<TypeStatement>>();
12✔
368
            this.enumerateBrsFiles((file) => {
12✔
369
                for (let stmt of file.parser.references.typeStatements) {
12✔
370
                    const lowerTypeName = stmt.fullName.toLowerCase();
5✔
371
                    //only track enums with a defined name (i.e. exclude nameless malformed enums)
372
                    if (lowerTypeName) {
5!
373
                        map.set(lowerTypeName, { item: stmt, file: file });
5✔
374
                    }
375
                }
376
            });
377
            return map;
12✔
378
        });
379
    }
380

381
    /**
382
     * The list of diagnostics found specifically for this scope. Individual file diagnostics are stored on the files themselves.
383
     */
384
    protected diagnostics = [] as BsDiagnostic[];
2,952✔
385

386
    protected onDependenciesChanged(event: DependencyChangedEvent) {
387
        this.logDebug('invalidated because dependency graph said [', event.sourceKey, '] changed');
2,585✔
388
        this.invalidate();
2,585✔
389
    }
390

391
    /**
392
     * Clean up all event handles
393
     */
394
    public dispose() {
395
        this.unsubscribeFromDependencyGraph?.();
4,099!
396
    }
397

398
    /**
399
     * Does this scope know about the given namespace name?
400
     * @param namespaceName - the name of the namespace (i.e. "NameA", or "NameA.NameB", etc...)
401
     */
402
    public isKnownNamespace(namespaceName: string) {
403
        let namespaceNameLower = namespaceName.toLowerCase();
×
404
        this.enumerateBrsFiles((file) => {
×
405
            for (let namespace of file.parser.references.namespaceStatements) {
×
406
                let loopNamespaceNameLower = namespace.name.toLowerCase();
×
407
                if (loopNamespaceNameLower === namespaceNameLower || loopNamespaceNameLower.startsWith(namespaceNameLower + '.')) {
×
408
                    return true;
×
409
                }
410
            }
411
        });
412
        return false;
×
413
    }
414

415
    /**
416
     * Get the parent scope for this scope (for source scope this will always be the globalScope).
417
     * XmlScope overrides this to return the parent xml scope if available.
418
     * For globalScope this will return null.
419
     */
420
    public getParentScope(): Scope | null {
421
        let scope: Scope | undefined;
422
        //use the global scope if we didn't find a sope and this is not the global scope
423
        if (this.program.globalScope !== this) {
6,173✔
424
            scope = this.program.globalScope;
1,870✔
425
        }
426
        if (scope) {
6,173✔
427
            return scope;
1,870✔
428
        } else {
429
            //passing null to the cache allows it to skip the factory function in the future
430
            return null;
4,303✔
431
        }
432
    }
433

434
    private dependencyGraph: DependencyGraph;
435
    /**
436
     * An unsubscribe function for the dependencyGraph subscription
437
     */
438
    private unsubscribeFromDependencyGraph: () => void;
439

440
    public attachDependencyGraph(dependencyGraph: DependencyGraph) {
441
        this.dependencyGraph = dependencyGraph;
2,952✔
442
        if (this.unsubscribeFromDependencyGraph) {
2,952✔
443
            this.unsubscribeFromDependencyGraph();
2✔
444
        }
445

446
        //anytime a dependency for this scope changes, we need to be revalidated
447
        this.unsubscribeFromDependencyGraph = this.dependencyGraph.onchange(this.dependencyGraphKey, this.onDependenciesChanged.bind(this));
2,952✔
448

449
        //invalidate immediately since this is a new scope
450
        this.invalidate();
2,952✔
451
    }
452

453
    /**
454
     * Get the file from this scope with the given path.
455
     * @param filePath can be a srcPath or pkgPath
456
     * @param normalizePath should this function repair and standardize the path? Passing false should have a performance boost if you can guarantee your path is already sanitized
457
     */
458
    public getFile<TFile extends BscFile>(filePath: string, normalizePath = true) {
11✔
459
        if (typeof filePath !== 'string') {
11!
460
            return undefined;
×
461
        }
462

463
        const key = path.isAbsolute(filePath) ? 'srcPath' : 'pkgPath';
11✔
464
        let map = this.cache.getOrAdd('fileMaps-srcPath', () => {
11✔
465
            const result = new Map<string, BscFile>();
7✔
466
            for (const file of this.getAllFiles()) {
7✔
467
                result.set(file[key].toLowerCase(), file);
11✔
468
            }
469
            return result;
7✔
470
        });
471
        return map.get(
11✔
472
            (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
11!
473
        ) as TFile;
474
    }
475

476
    /**
477
     * Get the list of files referenced by this scope that are actually loaded in the program.
478
     * Excludes files from ancestor scopes
479
     */
480
    public getOwnFiles() {
481
        //source scope only inherits files from global, so just return all files. This function mostly exists to assist XmlScope
482
        return this.getAllFiles();
22,103✔
483
    }
484

485
    /**
486
     * Get the list of files referenced by this scope that are actually loaded in the program.
487
     * Includes files from this scope and all ancestor scopes
488
     */
489
    public getAllFiles(): BscFile[] {
490
        return this.cache.getOrAdd('getAllFiles', () => {
16,533✔
491
            let result = [] as BscFile[];
1,154✔
492
            let dependencies = this.dependencyGraph.getAllDependencies(this.dependencyGraphKey);
1,154✔
493
            for (let dependency of dependencies) {
1,154✔
494
                //load components by their name
495
                if (dependency.startsWith('component:')) {
1,983✔
496
                    let comp = this.program.getComponent(dependency.replace(/$component:/, ''));
215✔
497
                    if (comp) {
215!
498
                        result.push(comp.file);
×
499
                    }
500
                } else {
501
                    let file = this.program.getFile(dependency);
1,768✔
502
                    if (file) {
1,768✔
503
                        result.push(file);
1,200✔
504
                    }
505
                }
506
            }
507
            this.logDebug('getAllFiles', () => result.map(x => x.pkgPath));
1,154✔
508
            return result;
1,154✔
509
        });
510
    }
511

512
    /**
513
     * Get the list of errors for this scope. It's calculated on the fly, so
514
     * call this sparingly.
515
     */
516
    public getDiagnostics() {
517
        let diagnosticLists = [this.diagnostics] as BsDiagnostic[][];
1,066✔
518

519
        //add diagnostics from every referenced file
520
        this.enumerateOwnFiles((file) => {
1,066✔
521
            diagnosticLists.push(file.getDiagnostics());
1,365✔
522
        });
523
        let allDiagnostics = Array.prototype.concat.apply([], diagnosticLists) as BsDiagnostic[];
1,066✔
524

525
        let filteredDiagnostics = allDiagnostics.filter((x) => {
1,066✔
526
            return !util.diagnosticIsSuppressed(x);
546✔
527
        });
528

529
        //filter out diangostics that match any of the comment flags
530

531
        return filteredDiagnostics;
1,066✔
532
    }
533

534
    public addDiagnostics(diagnostics: BsDiagnostic[]) {
535
        this.diagnostics.push(...diagnostics);
7,821✔
536
    }
537

538
    /**
539
     * Get the list of callables available in this scope (either declared in this scope or in a parent scope)
540
     */
541
    public getAllCallables(): CallableContainer[] {
542
        //get callables from parent scopes
543
        let parentScope = this.getParentScope();
3,864✔
544
        if (parentScope) {
3,864✔
545
            return [...this.getOwnCallables(), ...parentScope.getAllCallables()];
1,182✔
546
        } else {
547
            return [...this.getOwnCallables()];
2,682✔
548
        }
549
    }
550

551
    /**
552
     * Get the callable with the specified name.
553
     * If there are overridden callables with the same name, the closest callable to this scope is returned
554
     */
555
    public getCallableByName(name: string) {
556
        return this.getCallableMap().get(
158✔
557
            name.toLowerCase()
558
        );
559
    }
560

561
    public getCallableMap() {
562
        return this.cache.getOrAdd('callableMap', () => {
158✔
563
            const result = new Map<string, Callable>();
81✔
564
            for (let callable of this.getAllCallables()) {
81✔
565
                const callableName = callable.callable.getName(ParseMode.BrighterScript)?.toLowerCase();
6,347!
566
                result.set(callableName, callable.callable);
6,347✔
567
                result.set(
6,347✔
568
                    // Split by `.` and check the last term to consider namespaces.
569
                    callableName.split('.').pop()?.toLowerCase(),
19,041!
570
                    callable.callable
571
                );
572
            }
573
            return result;
81✔
574
        });
575
    }
576

577
    /**
578
     * Iterate over Brs files not shadowed by typedefs
579
     */
580
    public enumerateBrsFiles(callback: (file: BrsFile) => void) {
581
        const files = this.getAllFiles();
16,379✔
582
        for (const file of files) {
16,379✔
583
            //only brs files without a typedef
584
            if (isBrsFile(file) && !file.hasTypedef) {
18,228✔
585
                callback(file);
16,858✔
586
            }
587
        }
588
    }
589

590
    /**
591
     * Call a function for each file directly included in this scope (excluding files found only in parent scopes).
592
     */
593
    public enumerateOwnFiles(callback: (file: BscFile) => void) {
594
        const files = this.getOwnFiles();
10,043✔
595
        for (const file of files) {
10,043✔
596
            //either XML components or files without a typedef
597
            if (isXmlFile(file) || !file.hasTypedef) {
11,187✔
598
                callback(file);
11,161✔
599
            }
600
        }
601
    }
602

603
    /**
604
     * Get the list of callables explicitly defined in files in this scope.
605
     * This excludes ancestor callables
606
     */
607
    public getOwnCallables(): CallableContainer[] {
608
        let result = [] as CallableContainer[];
3,867✔
609
        this.logDebug('getOwnCallables() files: ', () => this.getOwnFiles().map(x => x.pkgPath));
3,867✔
610

611
        //get callables from own files
612
        this.enumerateOwnFiles((file) => {
3,867✔
613
            for (let callable of file?.callables ?? []) {
4,197!
614
                result.push({
207,598✔
615
                    callable: callable,
616
                    scope: this
617
                });
618
            }
619
        });
620
        return result;
3,867✔
621
    }
622

623
    /**
624
     * Builds a tree of namespace objects
625
     */
626
    public buildNamespaceLookup() {
627
        let namespaceLookup = new Map<string, NamespaceContainer>();
618✔
628
        this.enumerateBrsFiles((file) => {
618✔
629
            for (let namespaceStatement of file.parser.references.namespaceStatements) {
699✔
630
                //TODO should we handle non-brighterscript?
631
                let name = namespaceStatement.getName(ParseMode.BrighterScript);
239✔
632
                let nameParts = name.split('.');
239✔
633

634
                let loopName = null;
239✔
635
                //ensure each namespace section is represented in the results
636
                //(so if the namespace name is A.B.C, this will make an entry for "A", an entry for "A.B", and an entry for "A.B.C"
637
                for (let part of nameParts) {
239✔
638
                    loopName = loopName === null ? part : `${loopName}.${part}`;
352✔
639
                    let lowerLoopName = loopName.toLowerCase();
352✔
640
                    if (!namespaceLookup.has(lowerLoopName)) {
352✔
641
                        //only the always-needed fields are allocated up front; statement collections
642
                        //and the aggregate symbolTable are lazy-initialized below when a leaf
643
                        //declaration actually has something to put in them.
644
                        namespaceLookup.set(lowerLoopName, {
290✔
645
                            file: file,
646
                            fullName: loopName,
647
                            nameRange: namespaceStatement.nameExpression.range,
648
                            lastPartName: part,
649
                            namespaces: new Map()
650
                        });
651
                    }
652
                }
653
                let ns = namespaceLookup.get(name.toLowerCase());
239✔
654
                if (namespaceStatement.body.statements.length > 0) {
239✔
655
                    (ns.statements ??= []).push(...namespaceStatement.body.statements);
217✔
656
                }
657
                for (let statement of namespaceStatement.body.statements) {
239✔
658
                    if (isClassStatement(statement) && statement.name) {
296✔
659
                        (ns.classStatements ??= {})[statement.name.text.toLowerCase()] = statement;
61✔
660
                    } else if (isFunctionStatement(statement) && statement.name) {
235✔
661
                        (ns.functionStatements ??= {})[statement.name.text.toLowerCase()] = statement;
108✔
662
                    } else if (isEnumStatement(statement) && statement.fullName) {
127✔
663
                        (ns.enumStatements ??= new Map()).set(statement.fullName.toLowerCase(), statement);
38!
664
                    } else if (isConstStatement(statement) && statement.fullName) {
89✔
665
                        (ns.constStatements ??= new Map()).set(statement.fullName.toLowerCase(), statement);
53✔
666
                    }
667
                }
668
                // Merges all the symbol tables of the namespace statements into the new symbol table created above.
669
                // Set those symbol tables to have this new merged table as a parent.
670
                // The aggregate symbolTable is allocated here so every leaf gets one;
671
                // pure-intermediate containers (a name that's only a dotted prefix and never
672
                // a leaf of any namespaceStatement) will not have one allocated.
673
                ns.symbolTable ??= new SymbolTable(`Namespace Aggregate: '${ns.fullName}'`, () => this.symbolTable);
239✔
674
                ns.symbolTable.mergeSymbolTable(namespaceStatement.body.getSymbolTable());
239✔
675
            }
676

677
            //associate child namespaces with their parents
678
            for (let [, ns] of namespaceLookup) {
699✔
679
                let parts = ns.fullName.split('.');
316✔
680

681
                if (parts.length > 1) {
316✔
682
                    //remove the last part
683
                    parts.pop();
109✔
684
                    let parentName = parts.join('.');
109✔
685
                    const parent = namespaceLookup.get(parentName.toLowerCase());
109✔
686
                    parent.namespaces.set(ns.lastPartName.toLowerCase(), ns);
109✔
687
                }
688
            }
689
        });
690
        return namespaceLookup;
618✔
691
    }
692

693
    public getAllNamespaceStatements() {
694
        let result = [] as NamespaceStatement[];
11✔
695
        this.enumerateBrsFiles((file) => {
11✔
696
            result.push(...file.parser.references.namespaceStatements);
11✔
697
        });
698
        return result;
11✔
699
    }
700

701
    protected logDebug(...args: any[]) {
702
        this.program.logger.debug(this._debugLogComponentName, ...args);
8,598✔
703
    }
704
    private _debugLogComponentName: string;
705

706
    public validate(force = false) {
3,545✔
707
        //if this scope is already validated, no need to revalidate
708
        if (this.isValidated === true && !force) {
3,547✔
709
            this.logDebug('validate(): already validated');
990✔
710
            return;
990✔
711
        }
712

713
        this.program.logger.time(LogLevel.debug, [this._debugLogComponentName, 'validate()'], () => {
2,557✔
714

715
            let parentScope = this.getParentScope();
2,557✔
716

717
            //validate our parent before we validate ourself
718
            if (parentScope && parentScope.isValidated === false) {
2,557✔
719
                this.logDebug('validate(): validating parent first');
2✔
720
                parentScope.validate(force);
2✔
721
            }
722
            //clear the scope's errors list (we will populate them from this method)
723
            this.diagnostics = [];
2,557✔
724

725
            let callables = this.getAllCallables();
2,557✔
726

727
            //sort the callables by filepath and then method name, so the errors will be consistent
728
            // eslint-disable-next-line prefer-arrow-callback
729
            callables = callables.sort((a, b) => {
2,557✔
730
                const pathA = a.callable.file.srcPath;
199,109✔
731
                const pathB = b.callable.file.srcPath;
199,109✔
732
                //sort by path
733
                if (pathA < pathB) {
199,109✔
734
                    return -1;
58✔
735
                } else if (pathA > pathB) {
199,051✔
736
                    return 1;
867✔
737
                }
738
                //sort by function name
739
                const funcA = b.callable.name;
198,184✔
740
                const funcB = b.callable.name;
198,184✔
741
                if (funcA < funcB) {
198,184!
UNCOV
742
                    return -1;
×
743
                } else if (funcA > funcB) {
198,184!
UNCOV
744
                    return 1;
×
745
                }
746
                return 0;
198,184✔
747
            });
748

749
            //get a list of all callables, indexed by their lower case names
750
            let callableContainerMap = util.getCallableContainersByLowerName(callables);
2,557✔
751
            let files = this.getOwnFiles();
2,557✔
752

753
            //Since statements from files are shared across multiple scopes, we need to link those statements to the current scope
754
            this.linkSymbolTable();
2,557✔
755
            this.program.plugins.emit('beforeScopeValidate', this, files, callableContainerMap);
2,557✔
756

757
            this.program.plugins.emit('onScopeValidate', {
2,557✔
758
                program: this.program,
759
                scope: this
760
            });
761
            this._validate(callableContainerMap);
2,557✔
762

763
            this.program.plugins.emit('afterScopeValidate', this, files, callableContainerMap);
2,557✔
764
            //unlink all symbol tables from this scope (so they don't accidentally stick around)
765
            this.unlinkSymbolTable();
2,557✔
766

767
            (this as any).isValidated = true;
2,557✔
768
        });
769
    }
770

771
    protected _validate(callableContainerMap: CallableContainerMap) {
772
        //find all duplicate function declarations
773
        this.diagnosticFindDuplicateFunctionDeclarations(callableContainerMap);
2,557✔
774

775
        //detect missing and incorrect-case script imports
776
        this.diagnosticValidateScriptImportPaths();
2,557✔
777

778
        //enforce a series of checks on the bodies of class methods
779
        this.validateClasses();
2,557✔
780

781
        //do many per-file checks
782
        this.enumerateBrsFiles((file) => {
2,557✔
783
            this.diagnosticDetectFunctionCallsWithWrongParamCount(file, callableContainerMap);
2,594✔
784
            this.diagnosticDetectShadowedLocalVars(file, callableContainerMap);
2,594✔
785
            this.diagnosticDetectFunctionCollisions(file);
2,594✔
786
            this.detectVariableNamespaceCollisions(file);
2,594✔
787
            this.diagnosticDetectInvalidFunctionExpressionTypes(file);
2,594✔
788
        });
789
    }
790

791
    /**
792
     * Mark this scope as invalid, which means its `validate()` function needs to be called again before use.
793
     */
794
    public invalidate() {
795
        (this as any).isValidated = false;
5,537✔
796
        //clear out various lookups (they'll get regenerated on demand the next time they're requested)
797
        this.cache.clear();
5,537✔
798
    }
799

800
    public get symbolTable(): SymbolTable {
801
        return this.cache.getOrAdd('symbolTable', () => {
454✔
802
            const result = new SymbolTable(`Scope: '${this.name}'`, () => this.getParentScope()?.symbolTable);
281✔
803
            for (let file of this.getOwnFiles()) {
243✔
804
                if (isBrsFile(file)) {
345✔
805
                    result.mergeSymbolTable(file.parser?.symbolTable);
306!
806
                }
807
            }
808
            return result;
243✔
809
        });
810
    }
811

812
    /**
813
     * Builds the current symbol table for the scope, by merging the tables for all the files in this scope.
814
     * Also links all file symbols tables to this new table
815
     * This will only rebuilt if the symbol table has not been built before
816
     */
817
    public linkSymbolTable() {
818
        for (const file of this.getAllFiles()) {
4,605✔
819
            if (isBrsFile(file)) {
5,150✔
820
                file.parser.symbolTable.pushParentProvider(() => this.symbolTable);
4,688✔
821

822
                //link each NamespaceStatement's SymbolTable with the aggregate NamespaceLookup SymbolTable.
823
                //Leaf containers always have symbolTable populated (buildNamespaceLookup allocates one
824
                //before the merge step), so the lookup below is safe; the null guard exists only to
825
                //tolerate edge cases like a namespace that failed to register.
826
                for (const namespace of file.parser.references.namespaceStatements) {
4,688✔
827
                    const namespaceNameLower = namespace.getName(ParseMode.BrighterScript).toLowerCase();
420✔
828
                    const aggregate = this.namespaceLookup.get(namespaceNameLower)?.symbolTable;
420!
829
                    if (aggregate) {
420!
830
                        namespace.getSymbolTable().addSibling(aggregate);
420✔
831
                    }
832
                }
833
            }
834
        }
835
    }
836

837
    public unlinkSymbolTable() {
838
        for (let file of this.getOwnFiles()) {
4,585✔
839
            if (isBrsFile(file)) {
5,085✔
840
                file.parser?.symbolTable.popParentProvider();
4,627!
841

842
                for (const namespace of file.parser.references.namespaceStatements) {
4,627✔
843
                    const namespaceNameLower = namespace.getName(ParseMode.BrighterScript).toLowerCase();
411✔
844
                    const aggregate = this.namespaceLookup.get(namespaceNameLower)?.symbolTable;
411!
845
                    if (aggregate) {
411!
846
                        namespace.getSymbolTable().removeSibling(aggregate);
411✔
847
                    }
848
                }
849
            }
850
        }
851
    }
852

853
    private detectVariableNamespaceCollisions(file: BrsFile) {
854
        //find all function parameters
855
        for (let func of file.parser.references.functionExpressions) {
2,594✔
856
            for (let param of func.parameters) {
1,052✔
857
                let lowerParamName = param.name.text.toLowerCase();
552✔
858
                let namespace = this.getNamespace(lowerParamName, param.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript).toLowerCase());
552✔
859
                //see if the param matches any starting namespace part
860
                if (namespace) {
552✔
861
                    this.diagnostics.push({
4✔
862
                        file: file,
863
                        ...DiagnosticMessages.parameterMayNotHaveSameNameAsNamespace(param.name.text),
864
                        range: param.name.range,
865
                        relatedInformation: [{
866
                            message: 'Namespace declared here',
867
                            location: util.createLocation(
868
                                URI.file(namespace.file.srcPath).toString(),
869
                                namespace.nameRange
870
                            )
871
                        }]
872
                    });
873
                }
874
            }
875
        }
876

877
        for (let assignment of file.parser.references.assignmentStatements) {
2,594✔
878
            let lowerAssignmentName = assignment.name.text.toLowerCase();
400✔
879
            let namespace = this.getNamespace(lowerAssignmentName, assignment.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript).toLowerCase());
400✔
880
            //see if the param matches any starting namespace part
881
            if (namespace) {
400✔
882
                this.diagnostics.push({
4✔
883
                    file: file,
884
                    ...DiagnosticMessages.variableMayNotHaveSameNameAsNamespace(assignment.name.text),
885
                    range: assignment.name.range,
886
                    relatedInformation: [{
887
                        message: 'Namespace declared here',
888
                        location: util.createLocation(
889
                            URI.file(namespace.file.srcPath).toString(),
890
                            namespace.nameRange
891
                        )
892
                    }]
893
                });
894
            }
895
        }
896
    }
897

898
    /**
899
     * Find various function collisions
900
     */
901
    private diagnosticDetectFunctionCollisions(file: BscFile) {
902
        for (let func of file?.callables ?? []) {
2,594!
903
            const funcName = func.getName(ParseMode.BrighterScript);
119,244✔
904
            const lowerFuncName = funcName?.toLowerCase();
119,244!
905
            if (lowerFuncName) {
119,244!
906

907
                //find function declarations with the same name as a stdlib function
908
                if (globalCallableMap.has(lowerFuncName)) {
119,244✔
909
                    this.diagnostics.push({
118,350✔
910
                        ...DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction(),
911
                        range: func.nameRange,
912
                        file: file
913
                    });
914
                }
915

916
                //find any functions that have the same name as a class
917
                if (this.hasClass(lowerFuncName)) {
119,244✔
918
                    this.diagnostics.push({
2✔
919
                        ...DiagnosticMessages.functionCannotHaveSameNameAsClass(funcName),
920
                        range: func.nameRange,
921
                        file: file
922
                    });
923
                }
924
            }
925
        }
926
    }
927

928
    /**
929
     * Find function parameters and function return types that are neither built-in types or known Class references
930
     */
931
    private diagnosticDetectInvalidFunctionExpressionTypes(file: BrsFile) {
932
        for (let func of file.parser.references.functionExpressions) {
2,594✔
933
            // lazy-compute the namespace name only if a custom type is actually encountered
934
            let namespaceFetched = false;
1,052✔
935
            let currentNamespaceName: string;
936
            const getNamespaceName = () => {
1,052✔
937
                if (!namespaceFetched) {
42✔
938
                    namespaceFetched = true;
29✔
939
                    currentNamespaceName = func.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
29✔
940
                }
941
                return currentNamespaceName;
42✔
942
            };
943

944
            if (isCustomType(func.returnType) && func.returnTypeToken) {
1,052✔
945
                // check if this custom type is in our class map
946
                const returnTypeName = func.returnType.name;
23✔
947
                // check for built in types
948
                const isBuiltInType = util.isBuiltInType(returnTypeName);
23✔
949
                if (!isBuiltInType && !this.hasClass(returnTypeName, getNamespaceName()) && !this.hasInterface(returnTypeName) && !this.hasEnum(returnTypeName) && !this.hasTypeStatementType(returnTypeName)) {
23✔
950
                    this.diagnostics.push({
5✔
951
                        ...DiagnosticMessages.invalidFunctionReturnType(returnTypeName),
952
                        range: func.returnTypeToken.range,
953
                        file: file
954
                    });
955
                }
956
            }
957

958
            for (let param of func.parameters) {
1,052✔
959
                if (isCustomType(param.type) && param.typeToken) {
552✔
960
                    const paramTypeName = param.type.name;
28✔
961
                    // check for built in types
962
                    const isBuiltInType = util.isBuiltInType(paramTypeName);
28✔
963

964
                    if (!isBuiltInType && !this.hasClass(paramTypeName, getNamespaceName()) && !this.hasInterface(paramTypeName) && !this.hasEnum(paramTypeName) && !this.hasTypeStatementType(paramTypeName)) {
28✔
965
                        this.diagnostics.push({
2✔
966
                            ...DiagnosticMessages.functionParameterTypeIsInvalid(param.name.text, paramTypeName),
967
                            range: param.typeToken.range,
968
                            file: file
969
                        });
970

971
                    }
972
                }
973
            }
974
        }
975
    }
976

977
    public getNewExpressions() {
UNCOV
978
        let result = [] as AugmentedNewExpression[];
×
UNCOV
979
        this.enumerateBrsFiles((file) => {
×
UNCOV
980
            let expressions = file.parser.references.newExpressions as AugmentedNewExpression[];
×
UNCOV
981
            for (let expression of expressions) {
×
UNCOV
982
                expression.file = file;
×
UNCOV
983
                result.push(expression);
×
984
            }
985
        });
986
        return result;
×
987
    }
988

989
    private validateClasses() {
990
        let validator = new BsClassValidator(this);
2,557✔
991
        validator.validate();
2,557✔
992
        this.diagnostics.push(...validator.diagnostics);
2,557✔
993
    }
994

995
    /**
996
     * Detect calls to functions with the incorrect number of parameters
997
     */
998
    private diagnosticDetectFunctionCallsWithWrongParamCount(file: BscFile, callableContainersByLowerName: CallableContainerMap) {
999
        //validate all function calls
1000
        for (let expCall of file?.functionCalls ?? []) {
2,594!
1001
            let callableContainersWithThisName = callableContainersByLowerName.get(expCall.name.toLowerCase());
232✔
1002

1003
            //use the first item from callablesByLowerName, because if there are more, that's a separate error
1004
            let knownCallableContainer = callableContainersWithThisName ? callableContainersWithThisName[0] : undefined;
232✔
1005

1006
            if (knownCallableContainer) {
232✔
1007
                //get min/max parameter count for callable
1008
                let minParams = 0;
185✔
1009
                let maxParams = 0;
185✔
1010
                for (let param of knownCallableContainer.callable.params) {
185✔
1011
                    maxParams++;
461✔
1012
                    //optional parameters must come last, so we can assume that minParams won't increase once we hit
1013
                    //the first isOptional
1014
                    if (param.isOptional !== true) {
461✔
1015
                        minParams++;
191✔
1016
                    }
1017
                }
1018
                let expCallArgCount = expCall.args.length;
185✔
1019
                if (expCall.args.length > maxParams || expCall.args.length < minParams) {
185✔
1020
                    let minMaxParamsText = minParams === maxParams ? maxParams : `${minParams}-${maxParams}`;
14✔
1021
                    this.diagnostics.push({
14✔
1022
                        ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount),
1023
                        range: expCall.nameRange,
1024
                        //TODO detect end of expression call
1025
                        file: file
1026
                    });
1027
                }
1028
            }
1029
        }
1030
    }
1031

1032
    /**
1033
     * Detect local variables (function scope) that have the same name as scope calls
1034
     */
1035
    private diagnosticDetectShadowedLocalVars(file: BscFile, callableContainerMap: CallableContainerMap) {
1036
        const classMap = this.getClassMap();
2,594✔
1037
        //loop through every function scope
1038
        for (let scope of file.functionScopes) {
2,594✔
1039
            //every var declaration in this function scope
1040
            for (let varDeclaration of scope.variableDeclarations) {
1,052✔
1041
                const varName = varDeclaration.name;
958✔
1042
                const lowerVarName = varName.toLowerCase();
958✔
1043

1044
                //if the var is a function
1045
                if (isFunctionType(varDeclaration.type)) {
958✔
1046
                    //local var function with same name as stdlib function
1047
                    if (
23✔
1048
                        //has same name as stdlib
1049
                        globalCallableMap.has(lowerVarName)
1050
                    ) {
1051
                        this.diagnostics.push({
1✔
1052
                            ...DiagnosticMessages.localVarFunctionShadowsParentFunction('stdlib'),
1053
                            range: varDeclaration.nameRange,
1054
                            file: file
1055
                        });
1056

1057
                        //this check needs to come after the stdlib one, because the stdlib functions are included
1058
                        //in the scope function list
1059
                    } else if (
22✔
1060
                        //has same name as scope function
1061
                        callableContainerMap.has(lowerVarName)
1062
                    ) {
1063
                        this.diagnostics.push({
1✔
1064
                            ...DiagnosticMessages.localVarFunctionShadowsParentFunction('scope'),
1065
                            range: varDeclaration.nameRange,
1066
                            file: file
1067
                        });
1068
                    }
1069

1070
                    //var is not a function
1071
                } else if (
935✔
1072
                    //is NOT a callable from stdlib (because non-function local vars can have same name as stdlib names)
1073
                    !globalCallableMap.has(lowerVarName)
1074
                ) {
1075

1076
                    //is same name as a callable
1077
                    if (callableContainerMap.has(lowerVarName)) {
932✔
1078
                        this.diagnostics.push({
1✔
1079
                            ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1080
                            range: varDeclaration.nameRange,
1081
                            file: file
1082
                        });
1083
                        //has the same name as an in-scope class
1084
                    } else if (classMap.has(lowerVarName)) {
931✔
1085
                        this.diagnostics.push({
1✔
1086
                            ...DiagnosticMessages.localVarSameNameAsClass(classMap.get(lowerVarName)?.item.getName(ParseMode.BrighterScript)),
3!
1087
                            range: varDeclaration.nameRange,
1088
                            file: file
1089
                        });
1090
                    }
1091
                }
1092
            }
1093
        }
1094
    }
1095

1096
    /**
1097
     * Create diagnostics for any duplicate function declarations
1098
     */
1099
    private diagnosticFindDuplicateFunctionDeclarations(callableContainersByLowerName: CallableContainerMap) {
1100
        //for each list of callables with the same name
1101
        for (let [lowerName, callableContainers] of callableContainersByLowerName) {
2,557✔
1102

1103
            let globalCallables = [] as CallableContainer[];
187,545✔
1104
            let nonGlobalCallables = [] as CallableContainer[];
187,545✔
1105
            let ownCallables = [] as CallableContainer[];
187,545✔
1106
            let ancestorNonGlobalCallables = [] as CallableContainer[];
187,545✔
1107

1108
            for (let container of callableContainers) {
187,545✔
1109
                if (container.scope === this.program.globalScope) {
197,804✔
1110
                    globalCallables.push(container);
196,889✔
1111
                } else {
1112
                    nonGlobalCallables.push(container);
915✔
1113
                    if (container.scope === this) {
915✔
1114
                        ownCallables.push(container);
885✔
1115
                    } else {
1116
                        ancestorNonGlobalCallables.push(container);
30✔
1117
                    }
1118
                }
1119
            }
1120

1121
            //add info diagnostics about child shadowing parent functions
1122
            if (ownCallables.length > 0 && ancestorNonGlobalCallables.length > 0) {
187,545✔
1123
                for (let container of ownCallables) {
24✔
1124
                    //skip the init function (because every component will have one of those){
1125
                    if (lowerName !== 'init') {
24✔
1126
                        let shadowedCallable = ancestorNonGlobalCallables[ancestorNonGlobalCallables.length - 1];
23✔
1127
                        if (!!shadowedCallable && shadowedCallable.callable.file === container.callable.file) {
23✔
1128
                            //same file: skip redundant imports
1129
                            continue;
20✔
1130
                        }
1131
                        this.diagnostics.push({
3✔
1132
                            ...DiagnosticMessages.overridesAncestorFunction(
1133
                                container.callable.name,
1134
                                container.scope.name,
1135
                                shadowedCallable.callable.file.pkgPath,
1136
                                //grab the last item in the list, which should be the closest ancestor's version
1137
                                shadowedCallable.scope.name
1138
                            ),
1139
                            range: container.callable.nameRange,
1140
                            file: container.callable.file
1141
                        });
1142
                    }
1143
                }
1144
            }
1145

1146
            //add error diagnostics about duplicate functions in the same scope
1147
            if (ownCallables.length > 1) {
187,545✔
1148

1149
                for (let callableContainer of ownCallables) {
6✔
1150
                    let callable = callableContainer.callable;
12✔
1151

1152
                    this.diagnostics.push({
12✔
1153
                        ...DiagnosticMessages.duplicateFunctionImplementation(callable.name, callableContainer.scope.name),
1154
                        range: util.createRange(
1155
                            callable.nameRange.start.line,
1156
                            callable.nameRange.start.character,
1157
                            callable.nameRange.start.line,
1158
                            callable.nameRange.end.character
1159
                        ),
1160
                        file: callable.file
1161
                    });
1162
                }
1163
            }
1164
        }
1165
    }
1166

1167
    /**
1168
     * Get the list of all script imports for this scope
1169
     */
1170
    private getOwnScriptImports() {
1171
        let result = [] as FileReference[];
2,557✔
1172
        this.enumerateOwnFiles((file) => {
2,557✔
1173
            if (isBrsFile(file)) {
2,802✔
1174
                result.push(...file.ownScriptImports);
2,584✔
1175
            } else if (isXmlFile(file)) {
218!
1176
                result.push(...file.scriptTagImports);
218✔
1177
            }
1178
        });
1179
        return result;
2,557✔
1180
    }
1181

1182
    /**
1183
     * Verify that all of the scripts imported by each file in this scope actually exist
1184
     */
1185
    private diagnosticValidateScriptImportPaths() {
1186
        let scriptImports = this.getOwnScriptImports();
2,557✔
1187
        //verify every script import
1188
        for (let scriptImport of scriptImports) {
2,557✔
1189
            let referencedFile = this.getFileByRelativePath(scriptImport.pkgPath);
210✔
1190
            //if we can't find the file
1191
            if (!referencedFile) {
210✔
1192
                //skip the default bslib file, it will exist at transpile time but should not show up in the program during validation cycle
1193
                if (scriptImport.pkgPath === `source${path.sep}bslib.brs`) {
18✔
1194
                    continue;
2✔
1195
                }
1196
                let dInfo: DiagnosticInfo;
1197
                if (scriptImport.text.trim().length === 0) {
16!
UNCOV
1198
                    dInfo = DiagnosticMessages.scriptSrcCannotBeEmpty();
×
1199
                } else {
1200
                    dInfo = DiagnosticMessages.referencedFileDoesNotExist();
16✔
1201
                }
1202

1203
                this.diagnostics.push({
16✔
1204
                    ...dInfo,
1205
                    range: scriptImport.filePathRange,
1206
                    file: scriptImport.sourceFile
1207
                });
1208
                //if the character casing of the script import path does not match that of the actual path
1209
            } else if (scriptImport.pkgPath !== referencedFile.pkgPath) {
192✔
1210
                const correctUri = scriptImport.text.startsWith('pkg:/')
10✔
1211
                    ? util.getRokuPkgPath(referencedFile.pkgPath)
10✔
1212
                    : path.posix.relative(
1213
                        path.dirname(scriptImport.sourceFile.pkgPath).replace(/\\/g, '/'),
1214
                        referencedFile.pkgPath.replace(/\\/g, '/')
1215
                    );
1216
                this.diagnostics.push({
10✔
1217
                    ...DiagnosticMessages.scriptImportCaseMismatch(referencedFile.pkgPath, correctUri),
1218
                    range: scriptImport.filePathRange,
1219
                    file: scriptImport.sourceFile
1220
                });
1221
            }
1222
        }
1223
    }
1224

1225
    /**
1226
     * Find the file with the specified relative path
1227
     */
1228
    protected getFileByRelativePath(relativePath: string) {
1229
        if (!relativePath) {
210!
UNCOV
1230
            return;
×
1231
        }
1232
        let files = this.getAllFiles();
210✔
1233
        for (let file of files) {
210✔
1234
            if (file.pkgPath.toLowerCase() === relativePath.toLowerCase()) {
272✔
1235
                return file;
192✔
1236
            }
1237
        }
1238
    }
1239

1240
    /**
1241
     * Determine if this file is included in this scope (excluding parent scopes)
1242
     */
1243
    public hasFile(file: BscFile) {
1244
        let files = this.getOwnFiles();
8,235✔
1245
        let hasFile = files.includes(file);
8,235✔
1246
        return hasFile;
8,235✔
1247
    }
1248

1249
    /**
1250
     * Get all callables as completionItems
1251
     */
1252
    public getCallablesAsCompletions(parseMode: ParseMode) {
1253
        let completions = [] as CompletionItem[];
27✔
1254
        let callables = this.getAllCallables();
27✔
1255

1256
        if (parseMode === ParseMode.BrighterScript) {
27✔
1257
            //throw out the namespaced callables (they will be handled by another method)
1258
            callables = callables.filter(x => x.callable.hasNamespace === false);
862✔
1259
        }
1260

1261
        for (let callableContainer of callables) {
27✔
1262
            completions.push(this.createCompletionFromCallable(callableContainer));
1,266✔
1263
        }
1264
        return completions;
27✔
1265
    }
1266

1267
    public createCompletionFromCallable(callableContainer: CallableContainer): CompletionItem {
1268
        return {
1,266✔
1269
            label: callableContainer.callable.getName(ParseMode.BrighterScript),
1270
            kind: CompletionItemKind.Function,
1271
            detail: callableContainer.callable.shortDescription,
1272
            documentation: callableContainer.callable.documentation ? { kind: 'markdown', value: callableContainer.callable.documentation } : undefined
1,266✔
1273
        };
1274
    }
1275

1276
    public createCompletionFromFunctionStatement(statement: FunctionStatement): CompletionItem {
1277
        return {
12✔
1278
            label: statement.getName(ParseMode.BrighterScript),
1279
            kind: CompletionItemKind.Function
1280
        };
1281
    }
1282

1283
    /**
1284
     * Get the definition (where was this thing first defined) of the symbol under the position
1285
     * @deprecated use `DefinitionProvider.process()`
1286
     */
1287
    public getDefinition(file: BscFile, position: Position): Location[] {
1288
        // Overridden in XMLScope. Brs files use implementation in BrsFile
1289
        return [];
1✔
1290
    }
1291

1292
    /**
1293
     * Scan all files for property names, and return them as completions
1294
     */
1295
    public getPropertyNameCompletions() {
1296
        let results = [] as CompletionItem[];
5✔
1297
        this.enumerateBrsFiles((file) => {
5✔
1298
            results.push(...file.propertyNameCompletions);
5✔
1299
        });
1300
        return results;
5✔
1301
    }
1302

1303
    public getAllClassMemberCompletions() {
1304
        let results = new Map<string, CompletionItem>();
5✔
1305
        let filesSearched = new Set<BscFile>();
5✔
1306
        for (const file of this.getAllFiles()) {
5✔
1307
            if (isXmlFile(file) || filesSearched.has(file)) {
5!
UNCOV
1308
                continue;
×
1309
            }
1310
            filesSearched.add(file);
5✔
1311
            for (let cs of file.parser.references.classStatements) {
5✔
1312
                for (let s of [...cs.methods, ...cs.fields]) {
2✔
1313
                    if (!results.has(s.name.text) && s.name.text.toLowerCase() !== 'new') {
9✔
1314
                        results.set(s.name.text, {
8✔
1315
                            label: s.name.text,
1316
                            kind: isMethodStatement(s) ? CompletionItemKind.Method : CompletionItemKind.Field
8✔
1317
                        });
1318
                    }
1319
                }
1320
            }
1321
        }
1322
        return results;
5✔
1323
    }
1324

1325
    /**
1326
     * @param className - The name of the class (including namespace if possible)
1327
     * @param callsiteNamespace - the name of the namespace where the call site resides (this is NOT the known namespace of the class).
1328
     *                            This is used to help resolve non-namespaced class names that reside in the same namespac as the call site.
1329
     */
1330
    public getClassHierarchy(className: string, callsiteNamespace?: string) {
1331
        let items = [] as FileLink<ClassStatement>[];
2✔
1332
        let link = this.getClassFileLink(className, callsiteNamespace);
2✔
1333
        while (link) {
2✔
1334
            items.push(link);
3✔
1335
            link = this.getClassFileLink(link.item.parentClassName?.getName(ParseMode.BrighterScript)?.toLowerCase(), callsiteNamespace);
3✔
1336
        }
1337
        return items;
2✔
1338
    }
1339
}
1340

1341
/**
1342
 * A node in the per-scope namespace tree.
1343
 *
1344
 * `namespaces` is always allocated so parent-child wiring works for every container.
1345
 * The remaining collections are lazily allocated by `buildNamespaceLookup` only when
1346
 * a corresponding declaration is encountered, so pure-intermediate containers and
1347
 * sparsely-populated leaves do not pay the cost of empty Maps/Records they will never use.
1348
 *
1349
 * Consumers must handle these fields being undefined.
1350
 */
1351
export interface NamespaceContainer {
1352
    file: BscFile;
1353
    fullName: string;
1354
    nameRange: Range;
1355
    lastPartName: string;
1356
    namespaces: Map<string, NamespaceContainer>;
1357
    statements?: Statement[];
1358
    classStatements?: Record<string, ClassStatement>;
1359
    functionStatements?: Record<string, FunctionStatement>;
1360
    enumStatements?: Map<string, EnumStatement>;
1361
    constStatements?: Map<string, ConstStatement>;
1362
    symbolTable?: SymbolTable;
1363
}
1364

1365
interface AugmentedNewExpression extends NewExpression {
1366
    file: BscFile;
1367
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc