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

rokucommunity / brighterscript / #15132

28 Jan 2026 04:30PM UTC coverage: 87.198% (+0.005%) from 87.193%
#15132

push

web-flow
Merge 3366e9429 into 610607efc

14643 of 17747 branches covered (82.51%)

Branch coverage included in aggregate %.

76 of 78 new or added lines in 11 files covered. (97.44%)

200 existing lines in 8 files now uncovered.

15402 of 16709 relevant lines covered (92.18%)

24805.87 hits per line

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

86.55
/src/files/BrsFile.ts
1
import type { CodeWithSourceMap } from 'source-map';
2
import { SourceNode } from 'source-map';
1✔
3
import type { CompletionItem, Position, Location } from 'vscode-languageserver';
4
import { CancellationTokenSource } from 'vscode-languageserver';
1✔
5
import { CompletionItemKind } from 'vscode-languageserver';
1✔
6
import chalk from 'chalk';
1✔
7
import * as path from 'path';
1✔
8
import { DiagnosticCodeMap, diagnosticCodes, DiagnosticLegacyCodeMap, DiagnosticMessages } from '../DiagnosticMessages';
1✔
9
import { FunctionScope } from '../FunctionScope';
1✔
10
import type { Callable, CallableParam, CommentFlag, BsDiagnostic, FileReference, FileLink, SerializedCodeFile, NamespaceContainer } from '../interfaces';
11
import type { Token } from '../lexer/Token';
12
import { Lexer } from '../lexer/Lexer';
1✔
13
import { TokenKind, AllowedLocalIdentifiers } from '../lexer/TokenKind';
1✔
14
import { Parser, ParseMode } from '../parser/Parser';
1✔
15
import type { FunctionExpression } from '../parser/Expression';
16
import type { ClassStatement, NamespaceStatement, MethodStatement, FieldStatement } from '../parser/Statement';
17
import type { Program } from '../Program';
18
import { standardizePath as s, util } from '../util';
1✔
19
import { BrsTranspileState } from '../parser/BrsTranspileState';
1✔
20
import { serializeError } from 'serialize-error';
1✔
21
import { isDottedGetExpression, isFunctionExpression, isNamespaceStatement, isVariableExpression, isImportStatement, isAnyReferenceType, isNamespaceType, isReferenceType, isCallableType } from '../astUtils/reflection';
1✔
22
import { createVisitor, WalkMode } from '../astUtils/visitors';
1✔
23
import type { DependencyChangedEvent, DependencyGraph } from '../DependencyGraph';
24
import { CommentFlagProcessor } from '../CommentFlagProcessor';
1✔
25
import type { AstNode, Expression } from '../parser/AstNode';
26
import { ReferencesProvider } from '../bscPlugin/references/ReferencesProvider';
1✔
27
import { DocumentSymbolProcessor } from '../bscPlugin/symbols/DocumentSymbolProcessor';
1✔
28
import { WorkspaceSymbolProcessor } from '../bscPlugin/symbols/WorkspaceSymbolProcessor';
1✔
29
import type { UnresolvedSymbol, AssignedSymbol } from '../AstValidationSegmenter';
30
import { AstValidationSegmenter } from '../AstValidationSegmenter';
1✔
31
import { LogLevel } from '../Logger';
1✔
32
import type { BscSymbol } from '../SymbolTable';
33
import { SymbolTable } from '../SymbolTable';
1✔
34
import { SymbolTypeFlag } from '../SymbolTypeFlag';
1✔
35
import type { BscFileLike } from '../astUtils/CachedLookups';
36
import { CachedLookups } from '../astUtils/CachedLookups';
1✔
37
import { Editor } from '../astUtils/Editor';
1✔
38
import { getBsConst } from '../preprocessor/Manifest';
1✔
39
import type { BscType } from '../types';
40
import { NamespaceType } from '../types';
1✔
41
import type { BscFile } from './BscFile';
42
import { DefinitionProvider } from '../bscPlugin/definition/DefinitionProvider';
1✔
43

44
export interface ProvidedSymbol {
45
    symbol: BscSymbol;
46
    duplicates: BscSymbol[];
47
    requiredSymbolNames?: Set<string>;
48
}
49
export type ProvidedSymbolMap = Map<SymbolTypeFlag, Map<string, ProvidedSymbol>>;
50
export type ChangedSymbolMap = Map<SymbolTypeFlag, Set<string>>;
51

52
export interface ProvidedSymbolInfo {
53
    symbolMap: ProvidedSymbolMap;
54
    changes: ChangedSymbolMap;
55
}
56

57
/**
58
 * Holds all details about this file within the scope of the whole program
59
 */
60
export class BrsFile implements BscFile {
1✔
61
    constructor(options: {
62
        /**
63
         * The path to the file in its source location (where the source code lives in the file system)
64
         */
65
        srcPath: string;
66
        /**
67
         * The path to the file where it should exist in the program. This is similar to pkgPath, but retains its original file extensions from srcPath
68
         */
69
        destPath: string;
70
        /**
71
         * The final path in the zip. This has the extensions changed. Typically this is the same as destPath, but with file extensions changed for transpiled files.
72
         */
73
        pkgPath?: string;
74
        program: Program;
75
    }) {
76
        if (options) {
3,077!
77
            this.srcPath = s`${options.srcPath}`;
3,077✔
78
            this.destPath = s`${options.destPath}`;
3,077✔
79
            this.program = options.program;
3,077✔
80
            this._cachedLookups = new CachedLookups(this as unknown as BscFileLike);
3,077✔
81

82
            this.extension = util.getExtension(this.srcPath);
3,077✔
83
            if (options.pkgPath) {
3,077✔
84
                this.pkgPath = options.pkgPath;
343✔
85
            } else {
86
                //don't rename .d.bs files to .d.brs
87
                if (this.extension === '.d.bs') {
2,734✔
88
                    this.pkgPath = this.destPath;
21✔
89
                } else {
90
                    this.pkgPath = this.destPath.replace(/\.bs$/i, '.brs');
2,713✔
91
                }
92
            }
93

94
            //all BrighterScript files need to be transpiled
95
            if (this.extension?.endsWith('.bs') || this.program?.options?.allowBrighterScriptInBrightScript) {
3,077!
96
                this.parseMode = ParseMode.BrighterScript;
1,842✔
97
            }
98
            this.isTypedef = this.extension === '.d.bs';
3,077✔
99
            if (!this.isTypedef) {
3,077✔
100
                this.typedefKey = util.getTypedefPath(this.srcPath);
3,056✔
101
            }
102

103
            //global file doesn't have a program, so only resolve typedef info if we have a program
104
            if (this.program) {
3,077✔
105
                this.resolveTypedef();
3,073✔
106
            }
107
        }
108
    }
109

110
    public type = 'BrsFile';
3,077✔
111

112
    public srcPath: string;
113
    public destPath: string;
114
    public pkgPath: string;
115

116
    public program: Program;
117

118
    private _cachedLookups: CachedLookups;
119

120
    /**
121
     * An editor assigned during the build flow that manages edits that will be undone once the build process is complete.
122
     */
123
    public editor?: Editor;
124

125
    /**
126
     * Will this file result in only comment or whitespace output? If so, it can be excluded from the output if that bsconfig setting is enabled.
127
     */
128
    public get canBePruned() {
129
        let canPrune = true;
17✔
130
        this.ast.walk(createVisitor({
17✔
131
            FunctionStatement: () => {
132
                canPrune = false;
17✔
133
            },
134
            ClassStatement: () => {
135
                canPrune = false;
1✔
136
            }
137
        }), {
138
            walkMode: WalkMode.visitStatements
139
        });
140
        return canPrune;
17✔
141
    }
142

143
    /**
144
     * The parseMode used for the parser for this file
145
     */
146
    public parseMode = ParseMode.BrightScript;
3,077✔
147

148
    /**
149
     * The key used to identify this file in the dependency graph. This is set by the BrighterScript program and should not be set by plugins
150
     */
151
    public dependencyGraphKey: string;
152

153
    /**
154
     * Indicates whether this file needs to be validated.
155
     * Files are only ever validated a single time
156
     */
157
    public isValidated = false;
3,077✔
158

159
    /**
160
     * The all-lowercase extension for this file (including the leading dot)
161
     */
162
    public extension: string;
163

164
    public commentFlags = [] as CommentFlag[];
3,077✔
165

166
    private _functionScopes: FunctionScope[];
167

168
    public get functionScopes(): FunctionScope[] {
169
        if (!this._functionScopes) {
155✔
170
            this.createFunctionScopes();
101✔
171
        }
172
        return this._functionScopes;
155✔
173
    }
174

175
    private get cache() {
176
        // eslint-disable-next-line @typescript-eslint/dot-notation
177
        return this._cachedLookups['cache'];
61,654✔
178
    }
179

180
    /**
181
     * files referenced by import statements
182
     */
183
    public get ownScriptImports() {
184
        const result = this.cache?.getOrAdd('BrsFile_ownScriptImports', () => {
5,012!
185
            const result = [] as FileReference[];
4,377✔
186
            for (const statement of this._cachedLookups?.importStatements ?? []) {
4,377!
187
                //register import statements
188
                if (isImportStatement(statement) && statement.tokens.path) {
406✔
189
                    result.push({
404✔
190
                        filePathRange: statement.tokens.path.location?.range,
1,212✔
191
                        destPath: util.getPkgPathFromTarget(this.destPath, statement.filePath),
192
                        sourceFile: this,
193
                        text: statement.tokens.path.text
194
                    });
195
                }
196
            }
197
            return result;
4,377✔
198
        }) ?? [];
5,012!
199
        return result;
5,012✔
200
    }
201

202
    /**
203
     * Does this file need to be transpiled?
204
     * @deprecated use the `.editor` property to push changes to the file, which will force transpilation
205
     */
206
    public get needsTranspiled() {
207
        if (this._needsTranspiled !== undefined) {
758✔
208
            return this._needsTranspiled;
2✔
209
        }
210
        return !!(this.extension?.endsWith('.bs') || this.program?.options?.allowBrighterScriptInBrightScript || this.editor?.hasChanges);
756!
211
    }
212
    public set needsTranspiled(value) {
213
        this._needsTranspiled = value;
2✔
214
    }
215
    public _needsTranspiled: boolean;
216

217
    /**
218
     * The AST for this file
219
     */
220
    public get ast() {
221
        return this.parser?.ast;
16,047!
222
    }
223

224
    /**
225
     * Get the token at the specified position
226
     */
227
    public getTokenAt(position: Position) {
228
        for (let token of this.parser.tokens) {
276✔
229
            if (util.rangeContains(token.location?.range, position)) {
4,424!
230
                return token;
247✔
231
            }
232
        }
233
    }
234

235
    /**
236
     * Get the token at the specified position, or the next token
237
     */
238
    public getCurrentOrNextTokenAt(position: Position) {
239
        for (let token of this.parser.tokens) {
129✔
240
            if (util.comparePositionToRange(position, token.location?.range) < 0) {
1,959!
241
                return token;
128✔
242
            }
243
        }
244
    }
245

246
    /**
247
     * Walk the AST and find the expression that this token is most specifically contained within
248
     */
249
    public getClosestExpression(position: Position) {
250
        if (typeof position?.line !== 'number') {
257✔
251
            return undefined;
3✔
252
        }
253
        const handle = new CancellationTokenSource();
254✔
254
        let containingNode: AstNode;
255
        this.ast.walk((node) => {
254✔
256
            const latestContainer = containingNode;
4,189✔
257
            //bsc walks depth-first
258
            if (node.location?.range && util.rangeContains(node.location?.range, position)) {
4,189!
259
                containingNode = node;
1,183✔
260
            }
261
            //we had a match before, and don't now. this means we've finished walking down the whole way, and found our match
262
            if (latestContainer && !containingNode) {
4,189!
263
                containingNode = latestContainer;
×
UNCOV
264
                handle.cancel();
×
265
            }
266
        }, {
267
            walkMode: WalkMode.visitAllRecursive,
268
            cancel: handle.token
269
        });
270
        return containingNode;
254✔
271
    }
272

273
    public get parser() {
274
        if (!this._parser) {
45,480✔
275
            //remove the typedef file (if it exists)
276
            this.hasTypedef = false;
33✔
277
            this.typedefFile = undefined;
33✔
278

279
            //parse the file (it should parse fully since there's no linked typedef
280
            this.parse(this.fileContents);
33✔
281

282
            //re-link the typedef (if it exists...which it should)
283
            this.resolveTypedef();
33✔
284
        }
285
        return this._parser;
45,480✔
286
    }
287
    private _parser: Parser;
288

289
    public fileContents: string;
290

291
    /**
292
     * If this is a typedef file
293
     */
294
    public isTypedef: boolean;
295

296
    /**
297
     * The key to find the typedef file in the program's files map.
298
     * A falsey value means this file is ineligable for a typedef
299
     */
300
    public typedefKey?: string;
301

302
    /**
303
     * If the file was given type definitions during parse
304
     */
305
    public hasTypedef;
306

307
    /**
308
     * A reference to the typedef file (if one exists)
309
     */
310
    public typedefFile?: BrsFile;
311

312
    /**
313
     * Find and set the typedef variables (if a matching typedef file exists)
314
     */
315
    private resolveTypedef() {
316
        this.typedefFile = this.program.getFile<BrsFile>(this.typedefKey);
5,927✔
317
        this.hasTypedef = !!this.typedefFile;
5,927✔
318
    }
319

320
    public onDependenciesChanged(event: DependencyChangedEvent) {
321
        this.resolveTypedef();
2,821✔
322
    }
323

324
    /**
325
     * Attach the file to the dependency graph so it can monitor changes.
326
     * Also notify the dependency graph of our current dependencies so other dependents can be notified.
327
     * @deprecated this does nothing. This functionality is now handled by the file api and will be deleted in v1
328
     */
329
    public attachDependencyGraph(dependencyGraph: DependencyGraph) { }
330

331
    /**
332
     * The list of files that this file depends on
333
     */
334
    public get dependencies() {
335
        const result = this.ownScriptImports.filter(x => !!x.destPath).map(x => x.destPath.toLowerCase());
2,389✔
336

337
        //if this is a .brs file, watch for typedef changes
338
        if (this.extension === '.brs') {
2,389✔
339
            result.push(
551✔
340
                util.getTypedefPath(this.destPath)
341
            );
342
        }
343
        return result;
2,389✔
344
    }
345

346
    /**
347
     * Calculate the AST for this file
348
     * @param fileContents the raw source code to parse
349
     */
350
    public parse(fileContents: string) {
351
        const diagnostics = [] as Array<BsDiagnostic>;
2,811✔
352

353
        try {
2,811✔
354
            this.fileContents = fileContents;
2,811✔
355

356
            //if we have a typedef file, skip parsing this file
357
            if (this.hasTypedef) {
2,811✔
358
                //skip validation since the typedef is shadowing this file
359
                this.isValidated = true;
7✔
360
                return;
7✔
361
            }
362

363
            //tokenize the input file
364
            let lexer = this.program.logger.time('debug', ['lexer.lex', chalk.green(this.srcPath)], () => {
2,804✔
365
                return Lexer.scan(fileContents, {
2,803✔
366
                    includeWhitespace: false,
367
                    srcPath: this.srcPath
368
                });
369
            });
370

371
            this.getCommentFlags(lexer.tokens);
2,803✔
372

373
            this.program.logger.time(LogLevel.debug, ['parser.parse', chalk.green(this.srcPath)], () => {
2,803✔
374
                this._parser = Parser.parse(lexer.tokens, {
2,803✔
375
                    srcPath: this.srcPath,
376
                    mode: this.parseMode,
377
                    logger: this.program.logger,
378
                    bsConsts: getBsConst(this.program.getManifest())
379
                });
380
            });
381

382
            //absorb all lexing/preprocessing/parsing diagnostics
383
            diagnostics.push(
2,803✔
384
                ...lexer.diagnostics as BsDiagnostic[],
385
                ...this._parser.diagnostics as BsDiagnostic[]
386
            );
387

388

389
        } catch (e) {
390
            this._parser = new Parser();
1✔
391
            diagnostics.push({
1✔
392
                location: util.createLocationFromFileRange(this, util.createRange(0, 0, 0, Number.MAX_VALUE)),
393
                ...DiagnosticMessages.genericParserMessage('Critical error parsing file: ' + JSON.stringify(serializeError(e)))
394
            });
395
        }
396
        this.program?.diagnostics.register(diagnostics);
2,804✔
397
    }
398

399
    /**
400
     * Find a class. This scans all scopes for this file, and returns the first matching class that is found.
401
     * Returns undefined if not found.
402
     * @param className - The class name, including the namespace of the class if possible
403
     * @param containingNamespace - The namespace used to resolve relative class names. (i.e. the namespace around the current statement trying to find a class)
404
     * @returns the first class in the first scope found, or undefined if not found
405
     */
406
    public getClassFileLink(className: string, containingNamespace?: string): FileLink<ClassStatement> {
407
        const lowerClassName = className.toLowerCase();
258✔
408
        const lowerContainingNamespace = containingNamespace?.toLowerCase();
258✔
409

410
        const scopes = this.program.getScopesForFile(this);
258✔
411
        //find the first class in the first scope that has it
412
        for (let scope of scopes) {
258✔
413
            const cls = scope.getClassFileLink(lowerClassName, lowerContainingNamespace);
258✔
414
            if (cls) {
258✔
415
                return cls;
254✔
416
            }
417
        }
418
    }
419

420
    public findPropertyNameCompletions(): CompletionItem[] {
421
        //Build completion items from all the "properties" found in the file
422
        const { propertyHints } = this._cachedLookups;
5✔
423
        const results = [] as CompletionItem[];
5✔
424
        for (const key of Object.keys(propertyHints)) {
5✔
425
            results.push({
9✔
426
                label: propertyHints[key],
427
                kind: CompletionItemKind.Text
428
            });
429
        }
430
        return results;
5✔
431
    }
432

433
    private _propertyNameCompletions: CompletionItem[];
434

435
    public get propertyNameCompletions(): CompletionItem[] {
436
        if (!this._propertyNameCompletions) {
5!
437
            this._propertyNameCompletions = this.findPropertyNameCompletions();
5✔
438
        }
439
        return this._propertyNameCompletions;
5✔
440
    }
441

442
    /**
443
     * Find all comment flags in the source code. These enable or disable diagnostic messages.
444
     * @param tokens - an array of tokens of which to find `TokenKind.Comment` from
445
     */
446
    public getCommentFlags(tokens: Token[]) {
447
        const processor = new CommentFlagProcessor(this, ['rem', `'`], diagnosticCodes, [DiagnosticCodeMap.unknownDiagnosticCode, DiagnosticLegacyCodeMap.unknownDiagnosticCode]);
2,803✔
448

449
        this.commentFlags = [];
2,803✔
450
        for (let lexerToken of tokens) {
2,803✔
451
            for (let triviaToken of lexerToken.leadingTrivia ?? []) {
119,871!
452
                if (triviaToken.kind === TokenKind.Comment) {
99,546✔
453
                    processor.tryAdd(triviaToken.text, triviaToken.location?.range);
6,250!
454
                }
455
            }
456
        }
457
        this.commentFlags.push(...processor.commentFlags);
2,803✔
458
        this.program?.diagnostics.register(processor.diagnostics);
2,803!
459
    }
460

461
    public scopesByFunc = new Map<FunctionExpression, FunctionScope>();
3,077✔
462

463
    /**
464
     * Create a scope for every function in this file
465
     */
466
    private createFunctionScopes() {
467
        //find every function
468
        let functions = this._cachedLookups.functionExpressions;
101✔
469

470
        //create a functionScope for every function
471
        this._functionScopes = [];
101✔
472

473
        for (let func of functions) {
101✔
474
            let scope = new FunctionScope(func);
122✔
475

476
            //find parent function, and add this scope to it if found
477
            {
478
                let parentScope = this.scopesByFunc.get(
122✔
479
                    func.findAncestor<FunctionExpression>(isFunctionExpression)
480
                );
481

482
                //add this child scope to its parent
483
                if (parentScope) {
122✔
484
                    parentScope.childrenScopes.push(scope);
8✔
485
                }
486
                //store the parent scope for this scope
487
                scope.parentScope = parentScope;
122✔
488
            }
489

490
            //add every parameter
491
            for (let param of func.parameters) {
122✔
492
                scope.variableDeclarations.push({
76✔
493
                    nameRange: param.tokens.name.location?.range,
228!
494
                    lineIndex: param.tokens.name.location?.range?.start.line,
456!
495
                    name: param.tokens.name.text,
496
                    getType: () => {
UNCOV
497
                        return param.getType({ flags: SymbolTypeFlag.typetime });
×
498
                    }
499
                });
500
            }
501

502
            //add all of ForEachStatement loop varibales
503
            func.body?.walk(createVisitor({
122!
504
                ForEachStatement: (stmt) => {
505
                    scope.variableDeclarations.push({
6✔
506
                        nameRange: stmt.tokens.item.location?.range,
18!
507
                        lineIndex: stmt.tokens.item.location?.range?.start.line,
36!
508
                        name: stmt.tokens.item.text,
NEW
509
                        getType: () => stmt.getType({ flags: SymbolTypeFlag.runtime })
×
510
                    });
511
                },
512
                LabelStatement: (stmt) => {
513
                    const { name: identifier } = stmt.tokens;
2✔
514
                    scope.labelStatements.push({
2✔
515
                        nameRange: identifier.location?.range,
6!
516
                        lineIndex: identifier.location?.range?.start.line,
12!
517
                        name: identifier.text
518
                    });
519
                }
520
            }), {
521
                walkMode: WalkMode.visitStatements
522
            });
523

524
            this.scopesByFunc.set(func, scope);
122✔
525

526
            //find every statement in the scope
527
            this._functionScopes.push(scope);
122✔
528
        }
529

530
        //find every variable assignment in the whole file
531
        let assignmentStatements = this._cachedLookups.assignmentStatements;
101✔
532

533
        for (let statement of assignmentStatements) {
101✔
534

535
            //find this statement's function scope
536
            let scope = this.scopesByFunc.get(
153✔
537
                statement.findAncestor<FunctionExpression>(isFunctionExpression)
538
            );
539

540
            //skip variable declarations that are outside of any scope
541
            if (scope) {
153✔
542
                const variableName = statement.tokens.name;
152✔
543
                scope.variableDeclarations.push({
152✔
544
                    nameRange: variableName.location?.range,
456!
545
                    lineIndex: variableName.location?.range?.start.line,
912!
546
                    name: variableName.text,
547
                    getType: () => {
548
                        return statement.getType({ flags: SymbolTypeFlag.runtime });
5✔
549
                    }
550
                });
551
            }
552
        }
553
    }
554

555
    public staticCallables: Callable[];
556

557
    get callables(): Callable[] {
558

559
        if (this.staticCallables) {
7,103✔
560
            // globalFile can statically set the callables
561
            return this.staticCallables;
2,104✔
562
        }
563

564
        return this.cache.getOrAdd(`BrsFile_callables`, () => {
4,999✔
565
            const callables = [];
2,034✔
566

567
            for (let statement of this._cachedLookups.functionStatements ?? []) {
2,034!
568

569
                //extract the parameters
570
                let params = [] as CallableParam[];
2,065✔
571
                for (let param of statement.func.parameters) {
2,065✔
572
                    const paramType = param.getType({ flags: SymbolTypeFlag.typetime });
1,097✔
573

574
                    let callableParam = {
1,097✔
575
                        name: param.tokens.name?.text,
3,291!
576
                        type: paramType,
577
                        isOptional: !!param.defaultValue,
578
                        isRestArgument: false
579
                    };
580
                    params.push(callableParam);
1,097✔
581
                }
582
                const funcType = statement.getType({ flags: SymbolTypeFlag.typetime });
2,065✔
583
                callables.push({
2,065✔
584
                    isSub: statement.func.tokens.functionType?.text.toLowerCase() === 'sub',
6,195✔
585
                    name: statement.tokens.name?.text,
6,195!
586
                    nameRange: statement.tokens.name?.location?.range,
12,390!
587
                    file: this,
588
                    params: params,
589
                    range: statement.func.location?.range,
6,195✔
590
                    type: funcType,
591
                    getName: statement.getName.bind(statement),
592
                    hasNamespace: !!statement.findAncestor<NamespaceStatement>(isNamespaceStatement),
593
                    functionStatement: statement
594
                });
595
            }
596

597
            return callables;
2,034✔
598
        });
599

600

601
    }
602

603
    /**
604
     * Find the function scope at the given position.
605
     * @param position the position used to find the deepest scope that contains it
606
     */
607
    public getFunctionScopeAtPosition(position: Position): FunctionScope {
608
        return this.cache.getOrAdd(`functionScope-${position.line}:${position.character}`, () => {
129✔
609
            return this._getFunctionScopeAtPosition(position, this.functionScopes);
129✔
610
        });
611
    }
612

613
    public _getFunctionScopeAtPosition(position: Position, functionScopes?: FunctionScope[]): FunctionScope {
614
        if (!functionScopes) {
250!
UNCOV
615
            functionScopes = this.functionScopes;
×
616
        }
617
        for (let scope of functionScopes) {
250✔
618
            if (util.rangeContains(scope.range, position)) {
136✔
619
                //see if any of that scope's children match the position also, and give them priority
620
                let childScope = this._getFunctionScopeAtPosition(position, scope.childrenScopes);
121✔
621
                if (childScope) {
121✔
622
                    return childScope;
1✔
623
                } else {
624
                    return scope;
120✔
625
                }
626
            }
627
        }
628
    }
629

630
    /**
631
     * Find the NamespaceStatement enclosing the given position
632
     */
633
    public getNamespaceStatementForPosition(position: Position): NamespaceStatement {
634
        if (position) {
11,655✔
635
            return this.cache.getOrAdd(`namespaceStatementForPosition-${position.line}:${position.character}`, () => {
11,652✔
636
                let mostSpecificNamespace: NamespaceStatement;
637
                for (const statement of this._cachedLookups.namespaceStatements) {
10,369✔
638
                    if (util.rangeContains(statement.location?.range, position)) {
214!
639
                        if (mostSpecificNamespace) {
140✔
640
                            if (util.isRangeInRange(statement.location?.range, mostSpecificNamespace.location?.range)) {
3!
641
                                mostSpecificNamespace = statement;
3✔
642
                            }
643
                        } else {
644
                            mostSpecificNamespace = statement;
137✔
645
                        }
646
                    }
647
                }
648
                return mostSpecificNamespace;
10,369✔
649
            });
650
        }
651
    }
652

653
    /**
654
     * Given a current token, walk
655
     */
656
    public getPartialVariableName(currentToken: Token, excludeTokens: TokenKind[] = null) {
12✔
657
        let identifierAndDotKinds = [TokenKind.Identifier, ...AllowedLocalIdentifiers, TokenKind.Dot];
14✔
658

659
        //consume tokens backwards until we find something other than a dot or an identifier
660
        let tokens = [];
14✔
661
        const parser = this.parser;
14✔
662
        for (let i = parser.tokens.indexOf(currentToken); i >= 0; i--) {
14✔
663
            currentToken = parser.tokens[i];
62✔
664
            if (identifierAndDotKinds.includes(currentToken.kind) && (!excludeTokens || !excludeTokens.includes(currentToken.kind))) {
62✔
665
                tokens.unshift(currentToken.text);
48✔
666
            } else {
667
                break;
14✔
668
            }
669
        }
670

671
        //if we found name and dot tokens, join them together to make the namespace name
672
        if (tokens.length > 0) {
14!
673
            return tokens.join('');
14✔
674
        } else {
UNCOV
675
            return undefined;
×
676
        }
677
    }
678

679
    public isPositionNextToTokenKind(position: Position, tokenKind: TokenKind) {
680
        const closestToken = this.getClosestToken(position);
×
UNCOV
681
        return this.isTokenNextToTokenKind(closestToken, tokenKind);
×
682
    }
683

684
    public isTokenNextToTokenKind(closestToken: Token, tokenKind: TokenKind) {
685
        const previousToken = this.getPreviousToken(closestToken);
272✔
686
        const previousTokenKind = previousToken?.kind;
272!
687
        //next to matched token
688
        if (!closestToken || closestToken.kind === TokenKind.Eof) {
272!
UNCOV
689
            return false;
×
690
        } else if (closestToken.kind === tokenKind) {
272✔
691
            return true;
37✔
692
        } else if (closestToken.kind === TokenKind.Newline || previousTokenKind === TokenKind.Newline) {
235✔
693
            return false;
131✔
694
            //next to an identifier, which is next to token kind
695
        } else if (closestToken.kind === TokenKind.Identifier && previousTokenKind === tokenKind) {
104!
UNCOV
696
            return true;
×
697
        } else {
698
            return false;
104✔
699
        }
700
    }
701

702
    public getTokenBefore(currentToken: Token, tokenKind?: TokenKind): Token {
703
        const index = this.parser.tokens.indexOf(currentToken);
221✔
704
        if (!tokenKind) {
221✔
705
            return this.parser.tokens[index - 1];
187✔
706
        }
707
        for (let i = index - 1; i >= 0; i--) {
34✔
708
            currentToken = this.parser.tokens[i];
49✔
709
            if (currentToken.kind === TokenKind.Newline) {
49✔
710
                break;
6✔
711
            } else if (currentToken.kind === tokenKind) {
43✔
712
                return currentToken;
28✔
713
            }
714
        }
715
        return undefined;
6✔
716
    }
717

718
    public tokenFollows(currentToken: Token, tokenKind: TokenKind): boolean {
719
        const index = this.parser.tokens.indexOf(currentToken);
132✔
720
        if (index > 0) {
132!
721
            return this.parser.tokens[index - 1].kind === tokenKind;
132✔
722
        }
UNCOV
723
        return false;
×
724
    }
725

726
    public getTokensUntil(currentToken: Token, tokenKind: TokenKind, direction: -1 | 1 = 1) {
×
727
        let tokens = [];
×
728
        for (let i = this.parser.tokens.indexOf(currentToken); direction === -1 ? i >= 0 : i < this.parser.tokens.length; i += direction) {
×
729
            currentToken = this.parser.tokens[i];
×
730
            if (currentToken.kind === TokenKind.Newline || currentToken.kind === tokenKind) {
×
UNCOV
731
                break;
×
732
            }
UNCOV
733
            tokens.push(currentToken);
×
734
        }
UNCOV
735
        return tokens;
×
736
    }
737

738
    public getNextTokenByPredicate(currentToken: Token, test: (Token) => boolean, direction: -1 | 1 = 1) {
×
739
        for (let i = this.parser.tokens.indexOf(currentToken); direction === -1 ? i >= 0 : i < this.parser.tokens.length; i += direction) {
128!
740
            currentToken = this.parser.tokens[i];
156✔
741
            if (test(currentToken)) {
156✔
742
                return currentToken;
128✔
743
            }
744
        }
UNCOV
745
        return undefined;
×
746
    }
747

748
    public getPreviousToken(token: Token) {
749
        const parser = this.parser;
582✔
750
        let idx = parser.tokens.indexOf(token);
582✔
751
        return parser.tokens[idx - 1];
582✔
752
    }
753

754
    /**
755
     * Finds the first scope for this file, then returns true if there's a namespace with this name.
756
     * Returns false if no namespace was found with that name
757
     */
758
    public calleeStartsWithNamespace(callee: Expression) {
759
        let left = callee as AstNode;
1,005✔
760
        while (isDottedGetExpression(left)) {
1,005✔
761
            left = left.obj;
1,042✔
762
        }
763

764
        if (isVariableExpression(left)) {
1,005✔
765
            const leftType = left.getType({ flags: SymbolTypeFlag.runtime });
922✔
766
            if (isNamespaceType(leftType)) {
922✔
767
                // this is a namespace, but it might be aliased. Look it up and see if it has the same name
768
                if (leftType.name.toLowerCase() === left.tokens?.name?.text?.toLowerCase()) {
12!
769
                    return true;
11✔
770
                }
771
            }
772
        }
773
        return false;
994✔
774
    }
775

776
    /**
777
     * Get the token closest to the position. if no token is found, the previous token is returned
778
     */
779
    public getClosestToken(position: Position) {
780
        let tokens = this.parser.tokens;
122✔
781
        for (let i = 0; i < tokens.length; i++) {
122✔
782
            let token = tokens[i];
1,555✔
783
            if (util.rangeContains(token.location?.range, position)) {
1,555!
784
                return token;
122✔
785
            }
786
            //if the position less than this token range, then this position touches no token,
787
            if (util.positionIsGreaterThanRange(position, token.location?.range) === false) {
1,433!
UNCOV
788
                let t = tokens[i - 1];
×
789
                //return the token or the first token
UNCOV
790
                return t ? t : tokens[0];
×
791
            }
792
        }
793
        //return the last token
UNCOV
794
        return tokens[tokens.length - 1];
×
795
    }
796

797
    /**
798
     * Builds a list of document symbols for this file. Used by LanguageServer's onDocumentSymbol functionality
799
     * @deprecated use `DocumentSymbolProvider.process()` instead
800
     */
801
    public getDocumentSymbols() {
UNCOV
802
        return new DocumentSymbolProcessor({
×
803
            documentSymbols: [],
804
            file: this,
805
            program: this.program
806
        }).process();
807
    }
808

809
    /**
810
     * Builds a list of workspace symbols for this file. Used by LanguageServer's onWorkspaceSymbol functionality
811
     */
812
    public getWorkspaceSymbols() {
UNCOV
813
        return new WorkspaceSymbolProcessor({
×
814
            program: this.program,
815
            workspaceSymbols: []
816
        }).process();
817
    }
818

819
    /**
820
     * Given a position in a file, if the position is sitting on some type of identifier,
821
     * go to the definition of that identifier (where this thing was first defined)
822
     * @deprecated use `DefinitionProvider.process()` instead
823
     */
824
    public getDefinition(position: Position): Location[] {
UNCOV
825
        return new DefinitionProvider({
×
826
            program: this.program,
827
            file: this,
828
            position: position,
829
            definitions: []
830
        }).process();
831
    }
832

833
    public getClassMemberDefinitions(textToSearchFor: string, file: BrsFile): Location[] {
834
        let results: Location[] = [];
2✔
835
        //get class fields and members
836
        const statementHandler = (statement: MethodStatement) => {
2✔
837
            if (statement.getName(file.parseMode).toLowerCase() === textToSearchFor) {
1!
838
                results.push(util.createLocationFromRange(util.pathToUri(file.srcPath), statement.location?.range));
1!
839
            }
840
        };
841
        const fieldStatementHandler = (statement: FieldStatement) => {
2✔
842
            if (statement.tokens.name.text.toLowerCase() === textToSearchFor) {
×
UNCOV
843
                results.push(util.createLocationFromRange(util.pathToUri(file.srcPath), statement.location?.range));
×
844
            }
845
        };
846
        file.parser.ast.walk(createVisitor({
2✔
847
            MethodStatement: statementHandler,
848
            FieldStatement: fieldStatementHandler
849
        }), {
850
            walkMode: WalkMode.visitStatements
851
        });
852

853
        return results;
2✔
854
    }
855

856
    public getClassMethod(classStatement: ClassStatement, name: string, walkParents = true): MethodStatement | undefined {
61✔
857
        //TODO - would like to write this with getClassHieararchy; but got stuck on working out the scopes to use... :(
858
        let statement;
859
        const statementHandler = (e: MethodStatement) => {
132✔
860
            if (!statement && e.tokens.name.text.toLowerCase() === name.toLowerCase()) {
202✔
861
                statement = e;
131✔
862
            }
863
        };
864
        while (classStatement) {
132✔
865
            classStatement.walk(createVisitor({
152✔
866
                MethodStatement: statementHandler
867
            }), {
868
                walkMode: WalkMode.visitStatements
869
            });
870
            if (statement) {
152✔
871
                break;
131✔
872
            }
873
            if (walkParents && classStatement.parentClassName) {
21✔
874
                const nameParts = classStatement.parentClassName.getNameParts();
20✔
875
                classStatement = this.getClassFileLink(nameParts[nameParts.length - 1], nameParts.slice(0, -1).join('.'))?.item;
20!
876
            } else {
877
                break;
1✔
878
            }
879

880
        }
881
        return statement;
132✔
882
    }
883

884
    /**
885
     * Given a position in a file, if the position is sitting on some type of identifier,
886
     * look up all references of that identifier (every place that identifier is used across the whole app)
887
     * @deprecated use `ReferencesProvider.process()` instead
888
     */
889
    public getReferences(position: Position): Location[] {
UNCOV
890
        return new ReferencesProvider({
×
891
            program: this.program,
892
            file: this,
893
            position: position,
894
            references: []
895
        }).process();
896
    }
897

898
    /**
899
     * Generate the code, map, and typedef for this file
900
     */
901
    public serialize(): SerializedCodeFile {
902
        const result: SerializedCodeFile = {};
749✔
903

904
        const transpiled = this.transpile();
749✔
905
        if (typeof transpiled.code === 'string') {
749!
906
            result.code = transpiled.code;
749✔
907
        }
908
        if (transpiled.map) {
749✔
909
            result.map = transpiled.map.toString();
401✔
910
        }
911
        //generate the typedef (if this is not a typedef itself, and if enabled)
912
        if (!this.isTypedef && this.program.options.emitDefinitions) {
749✔
913
            result.typedef = this.getTypedef();
23✔
914
        }
915
        return result;
749✔
916
    }
917

918
    /**
919
     * Convert the brightscript/brighterscript source code into valid brightscript
920
     */
921
    public transpile(): CodeWithSourceMap {
922
        const state = new BrsTranspileState(this);
756✔
923
        state.editor = this.editor ?? new Editor();
756✔
924
        let transpileResult: SourceNode | undefined;
925

926
        if (this.needsTranspiled) {
756✔
927
            const astTranspile = this.ast.transpile(state);
742✔
928
            const trailingComments = [];
742✔
929
            if (util.hasLeadingComments(this.parser.eofToken)) {
742✔
930
                if (util.isLeadingCommentOnSameLine(this.ast.statements[this.ast.statements.length - 1]?.location, this.parser.eofToken)) {
4!
931
                    trailingComments.push(' ');
2✔
932
                } else {
933
                    trailingComments.push('\n');
2✔
934
                }
935
                trailingComments.push(...state.transpileLeadingComments(this.parser.eofToken));
4✔
936
            }
937

938
            transpileResult = util.sourceNodeFromTranspileResult(null, null, state.srcPath, [...astTranspile, ...trailingComments]);
742✔
939
        } else if (this.program.options.sourceMap) {
14✔
940
            //emit code as-is with a simple map to the original file location
941
            transpileResult = util.simpleMap(state.srcPath, this.fileContents);
6✔
942
        } else {
943
            //simple SourceNode wrapping the entire file to simplify the logic below
944
            transpileResult = new SourceNode(null, null, state.srcPath, this.fileContents);
8✔
945
        }
946

947
        //if we created an editor for this flow, undo the edits now
948
        if (!this.editor) {
756✔
949
            //undo any AST edits that the transpile cycle has made
950
            state.editor.undoAll();
7✔
951
        }
952

953
        if (this.program.options.sourceMap) {
756✔
954
            const stagingFileName = path.basename(state.srcPath).replace(/\.bs$/, '.brs');
408✔
955
            return new SourceNode(null, null, stagingFileName, [
408✔
956
                transpileResult,
957
                //add the sourcemap reference comment
958
                state.newline + `'//# sourceMappingURL=./${stagingFileName}.map`
959
            ]).toStringWithSourceMap({ file: stagingFileName });
960
        } else {
961
            return {
348✔
962
                code: transpileResult.toString(),
963
                map: undefined
964
            };
965
        }
966
    }
967

968
    public validationSegmenter = new AstValidationSegmenter(this);
3,077✔
969

970
    public getNamespaceSymbolTable(allowCache = true) {
7,598✔
971
        if (!allowCache) {
9,694✔
972
            return this.constructNamespaceSymbolTable();
2,096✔
973
        }
974
        return this.cache?.getOrAdd(`namespaceSymbolTable`, () => this.constructNamespaceSymbolTable());
7,598!
975
    }
976

977
    private constructNamespaceSymbolTable() {
978
        const nsTable = new SymbolTable(`File NamespaceTypes ${this.destPath}`, () => this.program?.globalScope.symbolTable);
13,229!
979
        this.populateNameSpaceSymbolTable(nsTable);
4,494✔
980
        return nsTable;
4,494✔
981
    }
982

983
    public processSymbolInformation() {
984
        // Get namespaces across imported files
985
        this.program.logger.debug('Processing symbol information', this.srcPath);
2,096✔
986

987
        const nsTable = this.getNamespaceSymbolTable(false);
2,096✔
988
        this.linkSymbolTableDisposables.push(this.ast.symbolTable.addSibling(nsTable));
2,096✔
989
        this.validationSegmenter.processTree(this.ast);
2,096✔
990
        this.program.addFileSymbolInfo(this);
2,096✔
991
        this.unlinkNamespaceSymbolTables();
2,096✔
992
    }
993

994
    public unlinkNamespaceSymbolTables() {
995
        for (let disposable of this.linkSymbolTableDisposables) {
2,096✔
996
            disposable();
2,096✔
997
        }
998
        this.linkSymbolTableDisposables = [];
2,096✔
999
    }
1000

1001
    private linkSymbolTableDisposables = [];
3,077✔
1002

1003

1004
    public populateNameSpaceSymbolTable(namespaceSymbolTable: SymbolTable) {
1005
        //Add namespace aggregates to namespace member tables
1006
        const namespaceTypesKnown = new Map<string, BscType>();
4,494✔
1007
        // eslint-disable-next-line no-bitwise
1008
        let getTypeOptions = { flags: SymbolTypeFlag.runtime | SymbolTypeFlag.typetime };
4,494✔
1009
        for (const [nsName, nsContainer] of this.getNamespaceLookupObject()) {
4,494✔
1010
            let currentNSType: BscType = null;
1,713✔
1011
            let parentNSType: BscType = null;
1,713✔
1012
            const existingNsStmt = nsContainer.namespaceStatements?.[0];
1,713!
1013

1014
            if (!nsContainer.isTopLevel) {
1,713✔
1015
                parentNSType = namespaceTypesKnown.get(nsContainer.parentNameLower);
664✔
1016
                if (!parentNSType) {
664!
1017
                    // we don't know about the parent namespace... uh, oh!
1018
                    this.program.logger.error(`Unable to find parent namespace type for namespace ${nsName}`);
×
UNCOV
1019
                    break;
×
1020
                }
1021
                currentNSType = parentNSType.getMemberType(nsContainer.fullNameLower, getTypeOptions);
664✔
1022
            } else {
1023
                currentNSType = namespaceSymbolTable.getSymbolType(nsContainer.fullNameLower, getTypeOptions);
1,049✔
1024
            }
1025
            if (!isNamespaceType(currentNSType)) {
1,713!
1026
                if (!currentNSType || isReferenceType(currentNSType) || isCallableType(currentNSType)) {
1,713!
1027
                    currentNSType = existingNsStmt
1,713✔
1028
                        ? existingNsStmt.getType(getTypeOptions)
1,713✔
1029
                        : new NamespaceType(nsName);
1030
                    if (parentNSType) {
1,713✔
1031
                        // adding as a member of existing NS
1032
                        parentNSType.addMember(nsContainer.lastPartName, { definingNode: existingNsStmt }, currentNSType, getTypeOptions.flags);
664✔
1033
                    } else {
1034
                        namespaceSymbolTable.addSymbol(nsContainer.lastPartName, { definingNode: existingNsStmt }, currentNSType, getTypeOptions.flags);
1,049✔
1035
                    }
1036
                } else {
1037
                    // Something else already used the name this namespace is using.
UNCOV
1038
                    continue;
×
1039
                }
1040
            } else {
1041
                // Existing known namespace
1042
            }
1043
            if (!namespaceTypesKnown.has(nsName)) {
1,713!
1044
                namespaceTypesKnown.set(nsName, currentNSType);
1,713✔
1045
            }
1046
            currentNSType.memberTable.addSibling(nsContainer.symbolTable);
1,713✔
1047
        }
1048
    }
1049

1050
    public get requiredSymbols() {
1051
        return this.cache.getOrAdd(`requiredSymbols`, () => {
7,335✔
1052
            this.program.logger.debug('Getting required symbols', this.srcPath);
2,127✔
1053

1054
            const allNeededSymbolSets = this.validationSegmenter.unresolvedSegmentsSymbols.values();
2,127✔
1055

1056
            const requiredSymbols: UnresolvedSymbol[] = [];
2,127✔
1057
            const addedSymbols = new Map<SymbolTypeFlag, Set<string>>();
2,127✔
1058
            addedSymbols.set(SymbolTypeFlag.runtime, new Set());
2,127✔
1059
            addedSymbols.set(SymbolTypeFlag.typetime, new Set());
2,127✔
1060
            for (const setOfSymbols of allNeededSymbolSets) {
2,127✔
1061
                for (const symbol of setOfSymbols) {
540✔
1062
                    const fullSymbolKey = symbol.typeChain.map(tce => tce.name).join('.').toLowerCase();
1,076✔
1063
                    const flag = symbol.endChainFlags;
641✔
1064
                    if (this.providedSymbols.symbolMap.get(flag)?.has(fullSymbolKey)) {
641!
1065
                        // this catches namespaced things
UNCOV
1066
                        continue;
×
1067
                    }
1068
                    const existingSymbol = this.ast.getSymbolTable().getSymbol(fullSymbolKey, flag);
641✔
1069
                    if (existingSymbol?.length > 0) {
641✔
1070
                        if (symbol[0]?.data?.isAlias) {
66!
1071
                            //catches aliases
UNCOV
1072
                            continue;
×
1073
                        }
1074
                    }
1075
                    if (!addedSymbols.get(flag)?.has(fullSymbolKey)) {
641!
1076
                        requiredSymbols.push(symbol);
586✔
1077
                        addedSymbols.get(flag)?.add(fullSymbolKey);
586!
1078
                    }
1079
                }
1080
            }
1081
            return requiredSymbols;
2,127✔
1082
        });
1083
    }
1084

1085
    public get providedSymbols() {
1086
        return this.cache?.getOrAdd(`providedSymbols`, () => {
14,652!
1087
            return this.getProvidedSymbols();
2,125✔
1088
        });
1089
    }
1090

1091
    public get assignedSymbols() {
1092
        return this.cache.getOrAdd(`assignedSymbols`, () => {
×
UNCOV
1093
            const allAssignedSymbolsEntries = this.validationSegmenter.assignedTokensInSegment.entries() ?? [];
×
1094

UNCOV
1095
            let allAssignedSymbolsSet: AssignedSymbol[] = [];
×
1096

1097
            for (const [_segment, assignedSymbolSet] of allAssignedSymbolsEntries) {
×
UNCOV
1098
                allAssignedSymbolsSet.push(...assignedSymbolSet.values());
×
1099
            }
UNCOV
1100
            return allAssignedSymbolsSet;
×
1101
        });
1102
    }
1103

1104
    private getProvidedSymbols() {
1105
        this.program.logger.debug('Getting provided symbols', this.srcPath);
2,125✔
1106

1107
        const symbolMap = new Map<SymbolTypeFlag, Map<string, ProvidedSymbol>>();
2,125✔
1108
        const runTimeSymbolMap = new Map<string, ProvidedSymbol>();
2,125✔
1109
        const typeTimeSymbolMap = new Map<string, ProvidedSymbol>();
2,125✔
1110
        const referenceSymbolMap = new Map<SymbolTypeFlag, Map<string, ProvidedSymbol>>();
2,125✔
1111
        const referenceRunTimeSymbolMap = new Map<string, ProvidedSymbol>();
2,125✔
1112
        const referenceTypeTimeSymbolMap = new Map<string, ProvidedSymbol>();
2,125✔
1113

1114
        const tablesToGetSymbolsFrom: Array<{ table: SymbolTable; namePrefixLower?: string }> = [
2,125✔
1115
            { table: this.parser.symbolTable },
1116
            ...this.parser.symbolTable.pocketTables
1117
        ];
1118

1119
        for (const namespaceStatement of this._cachedLookups.namespaceStatements) {
2,125✔
1120
            tablesToGetSymbolsFrom.push({
625✔
1121
                table: namespaceStatement.body.getSymbolTable(),
1122
                namePrefixLower: namespaceStatement.getName(ParseMode.BrighterScript).toLowerCase()
1123
            });
1124
        }
1125

1126
        function getAnyDuplicates(symbolNameLower: string, providedSymbolMap: Map<string, ProvidedSymbol>, referenceProvidedSymbolMap: Map<string, ProvidedSymbol>) {
1127
            if (symbolNameLower === 'm') {
4,261!
UNCOV
1128
                return [];
×
1129
            }
1130
            let duplicates = [] as Array<BscSymbol>;
4,261✔
1131
            let existingSymbol = providedSymbolMap.get(symbolNameLower);
4,261✔
1132
            if (existingSymbol) {
4,261✔
1133
                duplicates.push(existingSymbol.symbol, ...existingSymbol.duplicates);
18✔
1134
            }
1135
            existingSymbol = referenceProvidedSymbolMap.get(symbolNameLower);
4,261✔
1136
            if (existingSymbol) {
4,261!
UNCOV
1137
                duplicates.push(existingSymbol.symbol, ...existingSymbol.duplicates);
×
1138
            }
1139

1140
            return duplicates;
4,261✔
1141
        }
1142

1143
        for (const symbolTable of tablesToGetSymbolsFrom) {
2,125✔
1144
            const runTimeSymbols = symbolTable.table.getOwnSymbols(SymbolTypeFlag.runtime);
2,754✔
1145
            const typeTimeSymbols = symbolTable.table.getOwnSymbols(SymbolTypeFlag.typetime);
2,754✔
1146

1147
            for (const symbol of runTimeSymbols) {
2,754✔
1148
                const symbolNameLower = symbolTable.namePrefixLower
3,450✔
1149
                    ? `${symbolTable.namePrefixLower}.${symbol.name.toLowerCase()}`
3,450✔
1150
                    : symbol.name.toLowerCase();
1151
                if (symbolNameLower === 'm') {
3,450✔
1152
                    continue;
11✔
1153
                }
1154
                const duplicates = getAnyDuplicates(symbolNameLower, runTimeSymbolMap, referenceRunTimeSymbolMap);
3,439✔
1155

1156
                if (!isAnyReferenceType(symbol.type)) {
3,439✔
1157
                    runTimeSymbolMap.set(symbolNameLower, { symbol: symbol, duplicates: duplicates });
3,390✔
1158
                } else {
1159
                    referenceRunTimeSymbolMap.set(symbolNameLower, { symbol: symbol, duplicates: duplicates });
49✔
1160
                }
1161
            }
1162

1163
            for (const symbol of typeTimeSymbols) {
2,754✔
1164
                const symbolNameLower = symbolTable.namePrefixLower
822✔
1165
                    ? `${symbolTable.namePrefixLower}.${symbol.name.toLowerCase()}`
822✔
1166
                    : symbol.name.toLowerCase();
1167
                if (symbolNameLower === 'm') {
822!
UNCOV
1168
                    continue;
×
1169
                }
1170

1171
                const duplicates = getAnyDuplicates(symbolNameLower, typeTimeSymbolMap, referenceTypeTimeSymbolMap);
822✔
1172
                if (!isAnyReferenceType(symbol.type)) {
822✔
1173
                    const requiredSymbolTypes = new Set<BscType>();
798✔
1174
                    util.getCustomTypesInSymbolTree(requiredSymbolTypes, symbol.type);
798✔
1175
                    const requiredSymbolNames = new Set<string>();
798✔
1176
                    for (const requiredType of requiredSymbolTypes.values()) {
798✔
1177
                        requiredSymbolNames.add(requiredType.toString());
402✔
1178
                    }
1179
                    typeTimeSymbolMap.set(symbolNameLower, { symbol: symbol, duplicates: duplicates, requiredSymbolNames: requiredSymbolNames });
798✔
1180
                } else {
1181
                    referenceTypeTimeSymbolMap.set(symbolNameLower, { symbol: symbol, duplicates: duplicates });
24✔
1182
                }
1183
            }
1184
        }
1185

1186
        symbolMap.set(SymbolTypeFlag.runtime, runTimeSymbolMap);
2,125✔
1187
        symbolMap.set(SymbolTypeFlag.typetime, typeTimeSymbolMap);
2,125✔
1188

1189
        referenceSymbolMap.set(SymbolTypeFlag.runtime, referenceRunTimeSymbolMap);
2,125✔
1190
        referenceSymbolMap.set(SymbolTypeFlag.typetime, referenceTypeTimeSymbolMap);
2,125✔
1191

1192
        const changes = new Map<SymbolTypeFlag, Set<string>>();
2,125✔
1193
        changes.set(SymbolTypeFlag.runtime, new Set<string>());
2,125✔
1194
        changes.set(SymbolTypeFlag.typetime, new Set<string>());
2,125✔
1195
        const previouslyProvidedSymbols = this.program.getFileSymbolInfo(this)?.provides.symbolMap;
2,125✔
1196
        const previousSymbolsChecked = new Map<SymbolTypeFlag, Set<string>>();
2,125✔
1197
        previousSymbolsChecked.set(SymbolTypeFlag.runtime, new Set<string>());
2,125✔
1198
        previousSymbolsChecked.set(SymbolTypeFlag.typetime, new Set<string>());
2,125✔
1199

1200
        for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
2,125✔
1201
            const newSymbolMapForFlag = symbolMap.get(flag);
4,250✔
1202
            const oldSymbolMapForFlag = previouslyProvidedSymbols?.get(flag);
4,250✔
1203
            const previousSymbolsCheckedForFlag = previousSymbolsChecked.get(flag);
4,250✔
1204
            const changesForFlag = changes.get(flag);
4,250✔
1205
            if (!oldSymbolMapForFlag) {
4,250✔
1206
                for (const key of newSymbolMapForFlag.keys()) {
3,952✔
1207
                    changesForFlag.add(key);
3,970✔
1208
                }
1209
                continue;
3,952✔
1210

1211
            }
1212
            for (const [symbolKey, symbolObj] of newSymbolMapForFlag) {
298✔
1213
                const symbolType = symbolObj.symbol.type;
200✔
1214
                const previousType = oldSymbolMapForFlag?.get(symbolKey)?.symbol?.type;
200!
1215
                previousSymbolsCheckedForFlag.add(symbolKey);
200✔
1216
                if (!previousType) {
200✔
1217
                    changesForFlag.add(symbolKey);
28✔
1218
                    continue;
28✔
1219
                }
1220
                const data = {};
172✔
1221
                if (!symbolType.isEqual(previousType, data)) {
172✔
1222
                    changesForFlag.add(symbolKey);
61✔
1223
                }
1224
            }
1225
            for (const [symbolKey] of previouslyProvidedSymbols.get(flag)) {
298✔
1226
                if (!previousSymbolsCheckedForFlag.has(symbolKey)) {
226✔
1227
                    changesForFlag.add(symbolKey);
54✔
1228
                }
1229
            }
1230
        }
1231
        return {
2,125✔
1232
            symbolMap: symbolMap,
1233
            changes: changes,
1234
            referenceSymbolMap: referenceSymbolMap
1235
        };
1236
    }
1237

1238
    public markSegmentAsValidated(node: AstNode) {
1239
        this.validationSegmenter.markSegmentAsValidated(node);
3,908✔
1240
    }
1241

1242
    public getNamespaceLookupObject() {
1243
        if (!this.isValidated) {
11,712✔
1244
            return this.buildNamespaceLookup();
1,435✔
1245
        }
1246
        return this.cache.getOrAdd(`namespaceLookup`, () => {
10,277✔
1247
            const nsLookup = this.buildNamespaceLookup();
2,039✔
1248
            return nsLookup;
2,039✔
1249
        });
1250
    }
1251

1252
    private buildNamespaceLookup() {
1253
        const namespaceLookup = new Map<string, NamespaceContainer>();
3,474✔
1254
        for (let namespaceStatement of this._cachedLookups.namespaceStatements) {
3,474✔
1255
            let nameParts = namespaceStatement.getNameParts();
644✔
1256

1257
            let loopName: string = null;
644✔
1258
            let lowerLoopName: string = null;
644✔
1259
            let parentNameLower: string = null;
644✔
1260

1261
            //ensure each namespace section is represented in the results
1262
            //(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"
1263
            for (let i = 0; i < nameParts.length; i++) {
644✔
1264

1265
                let part = nameParts[i];
1,024✔
1266
                let lowerPartName = part.text.toLowerCase();
1,024✔
1267

1268
                if (i === 0) {
1,024✔
1269
                    loopName = part.text;
644✔
1270
                    lowerLoopName = lowerPartName;
644✔
1271
                } else {
1272
                    parentNameLower = lowerLoopName;
380✔
1273
                    loopName += '.' + part.text;
380✔
1274
                    lowerLoopName += '.' + lowerPartName;
380✔
1275
                }
1276
                if (!namespaceLookup.has(lowerLoopName)) {
1,024✔
1277
                    namespaceLookup.set(lowerLoopName, {
900✔
1278
                        isTopLevel: i === 0,
1279
                        file: this,
1280
                        fullName: loopName,
1281
                        fullNameLower: lowerLoopName,
1282
                        parentNameLower: parentNameLower,
1283
                        nameParts: nameParts.slice(0, i),
1284
                        nameRange: namespaceStatement.nameExpression.location?.range,
2,700✔
1285
                        lastPartName: part.text,
1286
                        lastPartNameLower: lowerPartName,
1287
                        namespaceStatements: [],
1288
                        // the aggregate symbol table should have no parent. It should include just the symbols of the namespace.
1289
                        symbolTable: new SymbolTable(`Namespace File Aggregate: '${loopName}'`)
1290
                    });
1291
                }
1292
            }
1293
            let ns = namespaceLookup.get(lowerLoopName);
644✔
1294
            ns.namespaceStatements.push(namespaceStatement);
644✔
1295
            // Merges all the symbol tables of the namespace statements into the new symbol table created above.
1296
            // Set those symbol tables to have this new merged table as a parent
1297
            ns.symbolTable.mergeSymbolTable(namespaceStatement.body.getSymbolTable());
644✔
1298
        }
1299
        return namespaceLookup;
3,474✔
1300
    }
1301

1302
    public getTypedef() {
1303
        const state = new BrsTranspileState(this);
36✔
1304
        const typedef = this.ast.getTypedef(state);
36✔
1305
        const programNode = util.sourceNodeFromTranspileResult(null, null, this.srcPath, typedef);
36✔
1306
        return programNode.toString();
36✔
1307
    }
1308

1309
    public dispose() {
1310
        this._parser?.dispose();
2,279✔
1311

1312
        //deleting these properties result in lower memory usage (garbage collection is magic!)
1313
        delete this.fileContents;
2,279✔
1314
        delete this._parser;
2,279✔
1315
        delete this._functionScopes;
2,279✔
1316
        delete this.scopesByFunc;
2,279✔
1317
    }
1318
}
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