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

rokucommunity / brighterscript / #13683

03 Feb 2025 04:19PM UTC coverage: 86.753% (-1.4%) from 88.185%
#13683

push

web-flow
Merge 34e72243e into 4afb6f658

12476 of 15203 branches covered (82.06%)

Branch coverage included in aggregate %.

7751 of 8408 new or added lines in 101 files covered. (92.19%)

85 existing lines in 17 files now uncovered.

13398 of 14622 relevant lines covered (91.63%)

34302.41 hits per line

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

86.69
/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 { Scope } from '../Scope';
1✔
9
import { DiagnosticCodeMap, diagnosticCodes, DiagnosticLegacyCodeMap, DiagnosticMessages } from '../DiagnosticMessages';
1✔
10
import { FunctionScope } from '../FunctionScope';
1✔
11
import type { Callable, CallableParam, CommentFlag, BsDiagnostic, FileReference, FileLink, SerializedCodeFile, NamespaceContainer } from '../interfaces';
12
import type { Token } from '../lexer/Token';
13
import { Lexer } from '../lexer/Lexer';
1✔
14
import { TokenKind, AllowedLocalIdentifiers } from '../lexer/TokenKind';
1✔
15
import { Parser, ParseMode } from '../parser/Parser';
1✔
16
import type { FunctionExpression } from '../parser/Expression';
17
import type { ClassStatement, NamespaceStatement, MethodStatement, FieldStatement } from '../parser/Statement';
18
import type { Program } from '../Program';
19
import { DynamicType } from '../types/DynamicType';
1✔
20
import { standardizePath as s, util } from '../util';
1✔
21
import { BrsTranspileState } from '../parser/BrsTranspileState';
1✔
22
import { serializeError } from 'serialize-error';
1✔
23
import { isClassStatement, isDottedGetExpression, isFunctionExpression, isFunctionStatement, isNamespaceStatement, isVariableExpression, isImportStatement, isEnumStatement, isConstStatement, isAnyReferenceType, isNamespaceType, isReferenceType, isCallableType } from '../astUtils/reflection';
1✔
24
import { createVisitor, WalkMode } from '../astUtils/visitors';
1✔
25
import type { DependencyChangedEvent, DependencyGraph } from '../DependencyGraph';
26
import { CommentFlagProcessor } from '../CommentFlagProcessor';
1✔
27
import type { AstNode, Expression } from '../parser/AstNode';
28
import { ReferencesProvider } from '../bscPlugin/references/ReferencesProvider';
1✔
29
import { DocumentSymbolProcessor } from '../bscPlugin/symbols/DocumentSymbolProcessor';
1✔
30
import { WorkspaceSymbolProcessor } from '../bscPlugin/symbols/WorkspaceSymbolProcessor';
1✔
31
import type { UnresolvedSymbol, AssignedSymbol } from '../AstValidationSegmenter';
32
import { AstValidationSegmenter } from '../AstValidationSegmenter';
1✔
33
import { LogLevel } from '../Logger';
1✔
34
import type { BscSymbol } from '../SymbolTable';
35
import { SymbolTable } from '../SymbolTable';
1✔
36
import { SymbolTypeFlag } from '../SymbolTypeFlag';
1✔
37
import type { BscFileLike } from '../astUtils/CachedLookups';
38
import { CachedLookups } from '../astUtils/CachedLookups';
1✔
39
import { Editor } from '../astUtils/Editor';
1✔
40
import { getBsConst } from '../preprocessor/Manifest';
1✔
41
import type { BscType } from '../types';
42
import { NamespaceType } from '../types';
1✔
43
import type { BscFile } from './BscFile';
44
import { DefinitionProvider } from '../bscPlugin/definition/DefinitionProvider';
1✔
45

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

54
export interface ProvidedSymbolInfo {
55
    symbolMap: ProvidedSymbolMap;
56
    changes: ChangedSymbolMap;
57
}
58

59
/**
60
 * Holds all details about this file within the scope of the whole program
61
 */
62
export class BrsFile implements BscFile {
1✔
63
    constructor(options: {
64
        /**
65
         * The path to the file in its source location (where the source code lives in the file system)
66
         */
67
        srcPath: string;
68
        /**
69
         * 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
70
         */
71
        destPath: string;
72
        /**
73
         * 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.
74
         */
75
        pkgPath?: string;
76
        program: Program;
77
    }) {
78
        if (options) {
2,685!
79
            this.srcPath = s`${options.srcPath}`;
2,685✔
80
            this.destPath = s`${options.destPath}`;
2,685✔
81
            this.program = options.program;
2,685✔
82
            this._cachedLookups = new CachedLookups(this as unknown as BscFileLike);
2,685✔
83

84
            this.extension = util.getExtension(this.srcPath);
2,685✔
85
            if (options.pkgPath) {
2,685✔
86
                this.pkgPath = options.pkgPath;
313✔
87
            } else {
88
                //don't rename .d.bs files to .d.brs
89
                if (this.extension === '.d.bs') {
2,372✔
90
                    this.pkgPath = this.destPath;
20✔
91
                } else {
92
                    this.pkgPath = this.destPath.replace(/\.bs$/i, '.brs');
2,352✔
93
                }
94
            }
95

96
            //all BrighterScript files need to be transpiled
97
            if (this.extension?.endsWith('.bs') || this.program?.options?.allowBrighterScriptInBrightScript) {
2,685!
98
                this.parseMode = ParseMode.BrighterScript;
1,588✔
99
            }
100
            this.isTypedef = this.extension === '.d.bs';
2,685✔
101
            if (!this.isTypedef) {
2,685✔
102
                this.typedefKey = util.getTypedefPath(this.srcPath);
2,665✔
103
            }
104

105
            //global file doesn't have a program, so only resolve typedef info if we have a program
106
            if (this.program) {
2,685✔
107
                this.resolveTypedef();
2,681✔
108
            }
109
        }
110
    }
111

112
    public type = 'BrsFile';
2,685✔
113

114
    public srcPath: string;
115
    public destPath: string;
116
    public pkgPath: string;
117

118
    public program: Program;
119

120
    private _cachedLookups: CachedLookups;
121

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

127
    /**
128
     * 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.
129
     */
130
    public get canBePruned() {
131
        let canPrune = true;
17✔
132
        this.ast.walk(createVisitor({
17✔
133
            FunctionStatement: () => {
134
                canPrune = false;
17✔
135
            },
136
            ClassStatement: () => {
137
                canPrune = false;
1✔
138
            }
139
        }), {
140
            walkMode: WalkMode.visitStatements
141
        });
142
        return canPrune;
17✔
143
    }
144

145
    /**
146
     * The parseMode used for the parser for this file
147
     */
148
    public parseMode = ParseMode.BrightScript;
2,685✔
149

150
    /**
151
     * 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
152
     */
153
    public dependencyGraphKey: string;
154

155
    /**
156
     * Indicates whether this file needs to be validated.
157
     * Files are only ever validated a single time
158
     */
159
    public isValidated = false;
2,685✔
160

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

166
    public commentFlags = [] as CommentFlag[];
2,685✔
167

168
    private _functionScopes: FunctionScope[];
169

170
    public get functionScopes(): FunctionScope[] {
171
        if (!this._functionScopes) {
125✔
172
            this.createFunctionScopes();
83✔
173
        }
174
        return this._functionScopes;
125✔
175
    }
176

177
    private get cache() {
178
        // eslint-disable-next-line @typescript-eslint/dot-notation
179
        return this._cachedLookups['cache'];
50,673✔
180
    }
181

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

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

219
    /**
220
     * The AST for this file
221
     */
222
    public get ast() {
223
        return this.parser?.ast;
13,710!
224
    }
225

226
    /**
227
     * Get the token at the specified position
228
     */
229
    public getTokenAt(position: Position) {
230
        for (let token of this.parser.tokens) {
244✔
231
            if (util.rangeContains(token.location?.range, position)) {
3,663!
232
                return token;
216✔
233
            }
234
        }
235
    }
236

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

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

272
    public get parser() {
273
        if (!this._parser) {
37,874✔
274
            //remove the typedef file (if it exists)
275
            this.hasTypedef = false;
5✔
276
            this.typedefFile = undefined;
5✔
277

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

281
            //re-link the typedef (if it exists...which it should)
282
            this.resolveTypedef();
5✔
283
        }
284
        return this._parser;
37,874✔
285
    }
286
    private _parser: Parser;
287

288
    public fileContents: string;
289

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

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

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

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

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

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

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

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

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

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

352
        try {
2,408✔
353
            this.fileContents = fileContents;
2,408✔
354

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

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

370
            this.getCommentFlags(lexer.tokens);
2,400✔
371

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

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

387

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

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

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

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

432
    private _propertyNameCompletions: CompletionItem[];
433

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

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

448
        this.commentFlags = [];
2,400✔
449
        for (let lexerToken of tokens) {
2,400✔
450
            for (let triviaToken of lexerToken.leadingTrivia ?? []) {
105,875!
451
                if (triviaToken.kind === TokenKind.Comment) {
87,937✔
452
                    processor.tryAdd(triviaToken.text, triviaToken.location?.range);
5,660!
453
                }
454
            }
455
        }
456
        this.commentFlags.push(...processor.commentFlags);
2,400✔
457
        this.program?.diagnostics.register(processor.diagnostics);
2,400!
458
    }
459

460
    public scopesByFunc = new Map<FunctionExpression, FunctionScope>();
2,685✔
461

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

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

472
        for (let func of functions) {
83✔
473
            let scope = new FunctionScope(func);
102✔
474

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

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

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

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

523
            this.scopesByFunc.set(func, scope);
102✔
524

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

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

532
        for (let statement of assignmentStatements) {
83✔
533

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

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

554
    public staticCallables: Callable[];
555

556
    get callables(): Callable[] {
557

558
        if (this.staticCallables) {
5,997✔
559
            // globalFile can statically set the callables
560
            return this.staticCallables;
1,790✔
561
        }
562

563
        return this.cache.getOrAdd(`BrsFile_callables`, () => {
4,207✔
564
            const callables = [];
1,705✔
565

566
            for (let statement of this._cachedLookups.functionStatements ?? []) {
1,705!
567

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

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

596
            return callables;
1,705✔
597
        });
598

599

600
    }
601

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

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

629
    /**
630
     * Find the NamespaceStatement enclosing the given position
631
     */
632
    public getNamespaceStatementForPosition(position: Position): NamespaceStatement {
633
        if (position) {
10,708✔
634
            return this.cache.getOrAdd(`namespaceStatementForPosition-${position.line}:${position.character}`, () => {
10,705✔
635
                for (const statement of this._cachedLookups.namespaceStatements) {
9,424✔
636
                    if (util.rangeContains(statement.location?.range, position)) {
162!
637
                        return statement;
120✔
638
                    }
639
                }
640
            });
641
        }
642
    }
643

644
    /**
645
     * Given a current token, walk
646
     */
647
    public getPartialVariableName(currentToken: Token, excludeTokens: TokenKind[] = null) {
12✔
648
        let identifierAndDotKinds = [TokenKind.Identifier, ...AllowedLocalIdentifiers, TokenKind.Dot];
14✔
649

650
        //consume tokens backwards until we find something other than a dot or an identifier
651
        let tokens = [];
14✔
652
        const parser = this.parser;
14✔
653
        for (let i = parser.tokens.indexOf(currentToken); i >= 0; i--) {
14✔
654
            currentToken = parser.tokens[i];
62✔
655
            if (identifierAndDotKinds.includes(currentToken.kind) && (!excludeTokens || !excludeTokens.includes(currentToken.kind))) {
62✔
656
                tokens.unshift(currentToken.text);
48✔
657
            } else {
658
                break;
14✔
659
            }
660
        }
661

662
        //if we found name and dot tokens, join them together to make the namespace name
663
        if (tokens.length > 0) {
14!
664
            return tokens.join('');
14✔
665
        } else {
UNCOV
666
            return undefined;
×
667
        }
668
    }
669

670
    public isPositionNextToTokenKind(position: Position, tokenKind: TokenKind) {
UNCOV
671
        const closestToken = this.getClosestToken(position);
×
NEW
672
        return this.isTokenNextToTokenKind(closestToken, tokenKind);
×
673
    }
674

675
    public isTokenNextToTokenKind(closestToken: Token, tokenKind: TokenKind) {
676
        const previousToken = this.getPreviousToken(closestToken);
263✔
677
        const previousTokenKind = previousToken?.kind;
263!
678
        //next to matched token
679
        if (!closestToken || closestToken.kind === TokenKind.Eof) {
263!
UNCOV
680
            return false;
×
681
        } else if (closestToken.kind === tokenKind) {
263✔
682
            return true;
34✔
683
        } else if (closestToken.kind === TokenKind.Newline || previousTokenKind === TokenKind.Newline) {
229✔
684
            return false;
131✔
685
            //next to an identifier, which is next to token kind
686
        } else if (closestToken.kind === TokenKind.Identifier && previousTokenKind === tokenKind) {
98!
UNCOV
687
            return true;
×
688
        } else {
689
            return false;
98✔
690
        }
691
    }
692

693
    public getTokenBefore(currentToken: Token, tokenKind?: TokenKind): Token {
694
        const index = this.parser.tokens.indexOf(currentToken);
196✔
695
        if (!tokenKind) {
196✔
696
            return this.parser.tokens[index - 1];
168✔
697
        }
698
        for (let i = index - 1; i >= 0; i--) {
28✔
699
            currentToken = this.parser.tokens[i];
43✔
700
            if (currentToken.kind === TokenKind.Newline) {
43✔
701
                break;
6✔
702
            } else if (currentToken.kind === tokenKind) {
37✔
703
                return currentToken;
22✔
704
            }
705
        }
706
        return undefined;
6✔
707
    }
708

709
    public tokenFollows(currentToken: Token, tokenKind: TokenKind): boolean {
710
        const index = this.parser.tokens.indexOf(currentToken);
121✔
711
        if (index > 0) {
121!
712
            return this.parser.tokens[index - 1].kind === tokenKind;
121✔
713
        }
UNCOV
714
        return false;
×
715
    }
716

717
    public getTokensUntil(currentToken: Token, tokenKind: TokenKind, direction: -1 | 1 = 1) {
×
718
        let tokens = [];
×
NEW
719
        for (let i = this.parser.tokens.indexOf(currentToken); direction === -1 ? i >= 0 : i < this.parser.tokens.length; i += direction) {
×
720
            currentToken = this.parser.tokens[i];
×
721
            if (currentToken.kind === TokenKind.Newline || currentToken.kind === tokenKind) {
×
722
                break;
×
723
            }
724
            tokens.push(currentToken);
×
725
        }
726
        return tokens;
×
727
    }
728

729
    public getNextTokenByPredicate(currentToken: Token, test: (Token) => boolean, direction: -1 | 1 = 1) {
×
730
        for (let i = this.parser.tokens.indexOf(currentToken); direction === -1 ? i >= 0 : i < this.parser.tokens.length; i += direction) {
117!
731
            currentToken = this.parser.tokens[i];
147✔
732
            if (test(currentToken)) {
147✔
733
                return currentToken;
117✔
734
            }
735
        }
NEW
736
        return undefined;
×
737
    }
738

739
    public getPreviousToken(token: Token) {
740
        const parser = this.parser;
557✔
741
        let idx = parser.tokens.indexOf(token);
557✔
742
        return parser.tokens[idx - 1];
557✔
743
    }
744

745
    /**
746
     * Find the first scope that has a namespace with this name.
747
     * Returns false if no namespace was found with that name
748
     */
749
    public calleeStartsWithNamespace(callee: Expression) {
750
        let left = callee as AstNode;
934✔
751
        while (isDottedGetExpression(left)) {
934✔
752
            left = left.obj;
968✔
753
        }
754

755
        if (isVariableExpression(left)) {
934✔
756
            const leftType = left.getType({ flags: SymbolTypeFlag.runtime });
851✔
757
            if (isNamespaceType(leftType)) {
851✔
758
                let lowerName = left.tokens.name.text.toLowerCase();
10✔
759
                //find the first scope that contains this namespace
760
                let scopes = this.program.getScopesForFile(this);
10✔
761

762
                //if this file does not belong to any scopes, make a temporary one to answer the question
763
                if (scopes.length === 0) {
10!
UNCOV
764
                    const scope = new Scope(`temporary-for-${this.pkgPath}`, this.program);
×
UNCOV
765
                    scope.getAllFiles = () => [this];
×
UNCOV
766
                    scope.getOwnFiles = scope.getAllFiles;
×
UNCOV
767
                    scopes.push(scope);
×
768
                }
769

770
                for (let scope of scopes) {
10✔
771
                    if (scope.namespaceLookup.has(lowerName)) {
10✔
772
                        return true;
9✔
773
                    }
774
                }
775
            }
776
        }
777
        return false;
925✔
778
    }
779

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

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

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

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

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

857
        return results;
2✔
858
    }
859

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

884
        }
885
        return statement;
132✔
886
    }
887

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

902
    /**
903
     * Generate the code, map, and typedef for this file
904
     */
905
    public serialize(): SerializedCodeFile {
906
        const result: SerializedCodeFile = {};
689✔
907

908
        const transpiled = this.transpile();
689✔
909
        if (typeof transpiled.code === 'string') {
689!
910
            result.code = transpiled.code;
689✔
911
        }
912
        if (transpiled.map) {
689✔
913
            result.map = transpiled.map.toString();
355✔
914
        }
915
        //generate the typedef (if this is not a typedef itself, and if enabled)
916
        if (!this.isTypedef && this.program.options.emitDefinitions) {
689✔
917
            result.typedef = this.getTypedef();
21✔
918
        }
919
        return result;
689✔
920
    }
921

922
    /**
923
     * Convert the brightscript/brighterscript source code into valid brightscript
924
     */
925
    public transpile(): CodeWithSourceMap {
926
        const state = new BrsTranspileState(this);
696✔
927
        state.editor = this.editor ?? new Editor();
696✔
928
        let transpileResult: SourceNode | undefined;
929

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

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

951
        //if we created an editor for this flow, undo the edits now
952
        if (!this.editor) {
696✔
953
            //undo any AST edits that the transpile cycle has made
954
            state.editor.undoAll();
7✔
955
        }
956

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

972
    public validationSegmenter = new AstValidationSegmenter(this);
2,685✔
973

974
    public getNamespaceSymbolTable(allowCache = true) {
6,920✔
975
        if (!allowCache) {
8,679✔
976
            return this.constructNamespaceSymbolTable();
1,759✔
977
        }
978
        return this.cache?.getOrAdd(`namespaceSymbolTable`, () => this.constructNamespaceSymbolTable());
6,920!
979
    }
980

981
    private constructNamespaceSymbolTable() {
982
        const nsTable = new SymbolTable(`File NamespaceTypes ${this.destPath}`, () => this.program?.globalScope.symbolTable);
7,221!
983
        this.populateNameSpaceSymbolTable(nsTable);
3,804✔
984
        return nsTable;
3,804✔
985
    }
986

987
    public processSymbolInformation() {
988
        // Get namespaces across imported files
989
        this.program.logger.debug('Processing symbol information', this.srcPath);
1,759✔
990

991
        const nsTable = this.getNamespaceSymbolTable(false);
1,759✔
992
        this.linkSymbolTableDisposables.push(this.ast.symbolTable.addSibling(nsTable));
1,759✔
993
        this.validationSegmenter.processTree(this.ast);
1,759✔
994
        this.program.addFileSymbolInfo(this);
1,758✔
995
        this.unlinkNamespaceSymbolTables();
1,758✔
996
    }
997

998
    public unlinkNamespaceSymbolTables() {
999
        for (let disposable of this.linkSymbolTableDisposables) {
1,758✔
1000
            disposable();
1,758✔
1001
        }
1002
        this.linkSymbolTableDisposables = [];
1,758✔
1003
    }
1004

1005
    private linkSymbolTableDisposables = [];
2,685✔
1006

1007

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

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

1054
    public get requiredSymbols() {
1055
        return this.cache.getOrAdd(`requiredSymbols`, () => {
5,854✔
1056
            this.program.logger.debug('Getting required symbols', this.srcPath);
1,764✔
1057

1058
            const allNeededSymbolSets = this.validationSegmenter.unresolvedSegmentsSymbols.values();
1,764✔
1059

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

1089
    public get providedSymbols() {
1090
        return this.cache?.getOrAdd(`providedSymbols`, () => {
10,416!
1091
            return this.getProvidedSymbols();
1,762✔
1092
        });
1093
    }
1094

1095
    public get assignedSymbols() {
NEW
1096
        return this.cache.getOrAdd(`assignedSymbols`, () => {
×
NEW
1097
            const allAssignedSymbolsEntries = this.validationSegmenter.assignedTokensInSegment.entries() ?? [];
×
1098

NEW
1099
            let allAssignedSymbolsSet: AssignedSymbol[] = [];
×
1100

NEW
1101
            for (const [_segment, assignedSymbolSet] of allAssignedSymbolsEntries) {
×
NEW
1102
                allAssignedSymbolsSet.push(...assignedSymbolSet.values());
×
1103
            }
NEW
1104
            return allAssignedSymbolsSet;
×
1105
        });
1106
    }
1107

1108
    private getProvidedSymbols() {
1109
        this.program.logger.debug('Getting provided symbols', this.srcPath);
1,762✔
1110

1111
        const symbolMap = new Map<SymbolTypeFlag, Map<string, ProvidedSymbol>>();
1,762✔
1112
        const runTimeSymbolMap = new Map<string, ProvidedSymbol>();
1,762✔
1113
        const typeTimeSymbolMap = new Map<string, ProvidedSymbol>();
1,762✔
1114
        const referenceSymbolMap = new Map<SymbolTypeFlag, Map<string, ProvidedSymbol>>();
1,762✔
1115
        const referenceRunTimeSymbolMap = new Map<string, ProvidedSymbol>();
1,762✔
1116
        const referenceTypeTimeSymbolMap = new Map<string, ProvidedSymbol>();
1,762✔
1117

1118
        const tablesToGetSymbolsFrom: Array<{ table: SymbolTable; namePrefixLower?: string }> = [{
1,762✔
1119
            table: this.parser.symbolTable
1120
        }];
1121

1122
        for (const namespaceStatement of this._cachedLookups.namespaceStatements) {
1,762✔
1123
            tablesToGetSymbolsFrom.push({
589✔
1124
                table: namespaceStatement.body.getSymbolTable(),
1125
                namePrefixLower: namespaceStatement.getName(ParseMode.BrighterScript).toLowerCase()
1126
            });
1127
        }
1128

1129
        function getAnyDuplicates(symbolNameLower: string, providedSymbolMap: Map<string, ProvidedSymbol>, referenceProvidedSymbolMap: Map<string, ProvidedSymbol>) {
1130
            if (symbolNameLower === 'm') {
3,733!
NEW
1131
                return [];
×
1132
            }
1133
            let duplicates = [] as Array<BscSymbol>;
3,733✔
1134
            let existingSymbol = providedSymbolMap.get(symbolNameLower);
3,733✔
1135
            if (existingSymbol) {
3,733✔
1136
                duplicates.push(existingSymbol.symbol, ...existingSymbol.duplicates);
17✔
1137
            }
1138
            existingSymbol = referenceProvidedSymbolMap.get(symbolNameLower);
3,733✔
1139
            if (existingSymbol) {
3,733!
NEW
1140
                duplicates.push(existingSymbol.symbol, ...existingSymbol.duplicates);
×
1141
            }
1142

1143
            return duplicates;
3,733✔
1144
        }
1145

1146
        for (const symbolTable of tablesToGetSymbolsFrom) {
1,762✔
1147
            const runTimeSymbols = symbolTable.table.getOwnSymbols(SymbolTypeFlag.runtime);
2,351✔
1148
            const typeTimeSymbols = symbolTable.table.getOwnSymbols(SymbolTypeFlag.typetime);
2,351✔
1149

1150
            for (const symbol of runTimeSymbols) {
2,351✔
1151
                const symbolNameLower = symbolTable.namePrefixLower
3,024✔
1152
                    ? `${symbolTable.namePrefixLower}.${symbol.name.toLowerCase()}`
3,024✔
1153
                    : symbol.name.toLowerCase();
1154
                if (symbolNameLower === 'm') {
3,024✔
1155
                    continue;
10✔
1156
                }
1157
                const duplicates = getAnyDuplicates(symbolNameLower, runTimeSymbolMap, referenceRunTimeSymbolMap);
3,014✔
1158

1159
                if (!isAnyReferenceType(symbol.type)) {
3,014✔
1160
                    runTimeSymbolMap.set(symbolNameLower, { symbol: symbol, duplicates: duplicates });
2,978✔
1161
                } else {
1162
                    referenceRunTimeSymbolMap.set(symbolNameLower, { symbol: symbol, duplicates: duplicates });
36✔
1163
                }
1164
            }
1165

1166
            for (const symbol of typeTimeSymbols) {
2,351✔
1167
                const symbolNameLower = symbolTable.namePrefixLower
719✔
1168
                    ? `${symbolTable.namePrefixLower}.${symbol.name.toLowerCase()}`
719✔
1169
                    : symbol.name.toLowerCase();
1170
                if (symbolNameLower === 'm') {
719!
NEW
1171
                    continue;
×
1172
                }
1173

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

1189
        symbolMap.set(SymbolTypeFlag.runtime, runTimeSymbolMap);
1,762✔
1190
        symbolMap.set(SymbolTypeFlag.typetime, typeTimeSymbolMap);
1,762✔
1191

1192
        referenceSymbolMap.set(SymbolTypeFlag.runtime, referenceRunTimeSymbolMap);
1,762✔
1193
        referenceSymbolMap.set(SymbolTypeFlag.typetime, referenceTypeTimeSymbolMap);
1,762✔
1194

1195
        const changes = new Map<SymbolTypeFlag, Set<string>>();
1,762✔
1196
        changes.set(SymbolTypeFlag.runtime, new Set<string>());
1,762✔
1197
        changes.set(SymbolTypeFlag.typetime, new Set<string>());
1,762✔
1198
        const previouslyProvidedSymbols = this.program.getFileSymbolInfo(this)?.provides.symbolMap;
1,762✔
1199
        const previousSymbolsChecked = new Map<SymbolTypeFlag, Set<string>>();
1,762✔
1200
        previousSymbolsChecked.set(SymbolTypeFlag.runtime, new Set<string>());
1,762✔
1201
        previousSymbolsChecked.set(SymbolTypeFlag.typetime, new Set<string>());
1,762✔
1202

1203
        for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
1,762✔
1204
            const newSymbolMapForFlag = symbolMap.get(flag);
3,524✔
1205
            const oldSymbolMapForFlag = previouslyProvidedSymbols?.get(flag);
3,524✔
1206
            const previousSymbolsCheckedForFlag = previousSymbolsChecked.get(flag);
3,524✔
1207
            const changesForFlag = changes.get(flag);
3,524✔
1208
            if (!oldSymbolMapForFlag) {
3,524✔
1209
                for (const key of newSymbolMapForFlag.keys()) {
3,318✔
1210
                    changesForFlag.add(key);
3,482✔
1211
                }
1212
                continue;
3,318✔
1213

1214
            }
1215
            for (const [symbolKey, symbolObj] of newSymbolMapForFlag) {
206✔
1216
                const symbolType = symbolObj.symbol.type;
174✔
1217
                const previousType = oldSymbolMapForFlag?.get(symbolKey)?.symbol?.type;
174!
1218
                previousSymbolsCheckedForFlag.add(symbolKey);
174✔
1219
                if (!previousType) {
174✔
1220
                    changesForFlag.add(symbolKey);
28✔
1221
                    continue;
28✔
1222
                }
1223
                const data = {};
146✔
1224
                if (!symbolType.isEqual(previousType, data)) {
146✔
1225
                    changesForFlag.add(symbolKey);
55✔
1226
                }
1227
            }
1228
            for (const [symbolKey] of previouslyProvidedSymbols.get(flag)) {
206✔
1229
                if (!previousSymbolsCheckedForFlag.has(symbolKey)) {
168✔
1230
                    changesForFlag.add(symbolKey);
22✔
1231
                }
1232
            }
1233
        }
1234
        return {
1,762✔
1235
            symbolMap: symbolMap,
1236
            changes: changes,
1237
            referenceSymbolMap: referenceSymbolMap
1238
        };
1239
    }
1240

1241
    public markSegmentAsValidated(node: AstNode) {
1242
        this.validationSegmenter.markSegmentAsValidated(node);
3,307✔
1243
    }
1244

1245
    public getNamespaceLookupObject() {
1246
        if (!this.isValidated) {
9,427✔
1247
            return this.buildNamespaceLookup();
1,256✔
1248
        }
1249
        return this.cache.getOrAdd(`namespaceLookup`, () => {
8,171✔
1250
            const nsLookup = this.buildNamespaceLookup();
1,702✔
1251
            return nsLookup;
1,702✔
1252
        });
1253
    }
1254

1255
    private buildNamespaceLookup() {
1256
        const namespaceLookup = new Map<string, NamespaceContainer>();
2,958✔
1257
        for (let namespaceStatement of this._cachedLookups.namespaceStatements) {
2,958✔
1258
            let nameParts = namespaceStatement.getNameParts();
607✔
1259

1260
            let loopName: string = null;
607✔
1261
            let lowerLoopName: string = null;
607✔
1262
            let parentNameLower: string = null;
607✔
1263

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

1268
                let part = nameParts[i];
977✔
1269
                let lowerPartName = part.text.toLowerCase();
977✔
1270

1271
                if (i === 0) {
977✔
1272
                    loopName = part.text;
607✔
1273
                    lowerLoopName = lowerPartName;
607✔
1274
                } else {
1275
                    parentNameLower = lowerLoopName;
370✔
1276
                    loopName += '.' + part.text;
370✔
1277
                    lowerLoopName += '.' + lowerPartName;
370✔
1278
                }
1279
                if (!namespaceLookup.has(lowerLoopName)) {
977✔
1280
                    namespaceLookup.set(lowerLoopName, {
856✔
1281
                        isTopLevel: i === 0,
1282
                        file: this,
1283
                        fullName: loopName,
1284
                        fullNameLower: lowerLoopName,
1285
                        parentNameLower: parentNameLower,
1286
                        nameParts: nameParts.slice(0, i),
1287
                        nameRange: namespaceStatement.nameExpression.location?.range,
2,568✔
1288
                        lastPartName: part.text,
1289
                        lastPartNameLower: lowerPartName,
1290
                        functionStatements: new Map(),
1291
                        namespaceStatements: [],
1292
                        namespaces: new Map(),
1293
                        classStatements: new Map(),
1294
                        enumStatements: new Map(),
1295
                        constStatements: new Map(),
1296
                        statements: [],
1297
                        // the aggregate symbol table should have no parent. It should include just the symbols of the namespace.
1298
                        symbolTable: new SymbolTable(`Namespace Aggregate: '${loopName}'`)
1299
                    });
1300
                }
1301
            }
1302
            let ns = namespaceLookup.get(lowerLoopName);
607✔
1303
            ns.namespaceStatements.push(namespaceStatement);
607✔
1304
            ns.statements.push(...namespaceStatement.body.statements);
607✔
1305
            for (let statement of namespaceStatement.body.statements) {
607✔
1306
                if (isClassStatement(statement) && statement.tokens.name) {
748✔
1307
                    ns.classStatements.set(statement.tokens.name.text.toLowerCase(), statement);
128✔
1308
                } else if (isFunctionStatement(statement) && statement.tokens.name) {
620✔
1309
                    ns.functionStatements.set(statement.tokens.name.text.toLowerCase(), statement);
415✔
1310
                } else if (isEnumStatement(statement) && statement.fullName) {
205✔
1311
                    ns.enumStatements.set(statement.fullName.toLowerCase(), statement);
40✔
1312
                } else if (isConstStatement(statement) && statement.fullName) {
165✔
1313
                    ns.constStatements.set(statement.fullName.toLowerCase(), statement);
98✔
1314
                }
1315
            }
1316
            // Merges all the symbol tables of the namespace statements into the new symbol table created above.
1317
            // Set those symbol tables to have this new merged table as a parent
1318
            ns.symbolTable.mergeSymbolTable(namespaceStatement.body.getSymbolTable());
607✔
1319
        }
1320
        return namespaceLookup;
2,958✔
1321
    }
1322

1323
    public getTypedef() {
1324
        const state = new BrsTranspileState(this);
34✔
1325
        const typedef = this.ast.getTypedef(state);
34✔
1326
        const programNode = util.sourceNodeFromTranspileResult(null, null, this.srcPath, typedef);
34✔
1327
        return programNode.toString();
34✔
1328
    }
1329

1330
    public dispose() {
1331
        this._parser?.dispose();
1,894✔
1332

1333
        //deleting these properties result in lower memory usage (garbage collection is magic!)
1334
        delete this.fileContents;
1,894✔
1335
        delete this._parser;
1,894✔
1336
        delete this._functionScopes;
1,894✔
1337
        delete this.scopesByFunc;
1,894✔
1338
    }
1339
}
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