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

rokucommunity / brighterscript / #13117

01 Oct 2024 08:24AM UTC coverage: 86.842% (-1.4%) from 88.193%
#13117

push

web-flow
Merge abd960cd5 into 3a2dc7282

11537 of 14048 branches covered (82.13%)

Branch coverage included in aggregate %.

6991 of 7582 new or added lines in 100 files covered. (92.21%)

83 existing lines in 18 files now uncovered.

12692 of 13852 relevant lines covered (91.63%)

29478.96 hits per line

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

87.07
/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, 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 { DynamicType } from '../types/DynamicType';
1✔
19
import { standardizePath as s, util } from '../util';
1✔
20
import { BrsTranspileState } from '../parser/BrsTranspileState';
1✔
21
import { serializeError } from 'serialize-error';
1✔
22
import { isClassStatement, isDottedGetExpression, isFunctionExpression, isFunctionStatement, isNamespaceStatement, isVariableExpression, isImportStatement, isEnumStatement, isConstStatement, isAnyReferenceType, isNamespaceType, isReferenceType, isCallableType } from '../astUtils/reflection';
1✔
23
import { createVisitor, WalkMode } from '../astUtils/visitors';
1✔
24
import type { DependencyChangedEvent, DependencyGraph } from '../DependencyGraph';
25
import { CommentFlagProcessor } from '../CommentFlagProcessor';
1✔
26
import type { AstNode, Expression } from '../parser/AstNode';
27
import { ReferencesProvider } from '../bscPlugin/references/ReferencesProvider';
1✔
28
import { DocumentSymbolProcessor } from '../bscPlugin/symbols/DocumentSymbolProcessor';
1✔
29
import { WorkspaceSymbolProcessor } from '../bscPlugin/symbols/WorkspaceSymbolProcessor';
1✔
30
import type { UnresolvedSymbol, AssignedSymbol } from '../AstValidationSegmenter';
31
import { AstValidationSegmenter } from '../AstValidationSegmenter';
1✔
32
import { LogLevel } from '../Logger';
1✔
33
import type { BscSymbol } from '../SymbolTable';
34
import { SymbolTable } from '../SymbolTable';
1✔
35
import { SymbolTypeFlag } from '../SymbolTypeFlag';
1✔
36
import type { BscFileLike } from '../astUtils/CachedLookups';
37
import { CachedLookups } from '../astUtils/CachedLookups';
1✔
38
import { Editor } from '../astUtils/Editor';
1✔
39
import { getBsConst } from '../preprocessor/Manifest';
1✔
40
import type { BscType } from '../types';
41
import { NamespaceType } from '../types';
1✔
42
import type { BscFile } from './BscFile';
43
import { DefinitionProvider } from '../bscPlugin/definition/DefinitionProvider';
1✔
44

45
export interface ProvidedSymbol {
46
    symbol: BscSymbol;
47
    duplicates: BscSymbol[];
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) {
2,492!
77
            this.srcPath = s`${options.srcPath}`;
2,492✔
78
            this.destPath = s`${options.destPath}`;
2,492✔
79
            this.program = options.program;
2,492✔
80
            this._cachedLookups = new CachedLookups(this as unknown as BscFileLike);
2,492✔
81

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

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

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

110
    public type = 'BrsFile';
2,492✔
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;
2,492✔
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;
2,492✔
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[];
2,492✔
165

166
    private _functionScopes: FunctionScope[];
167

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

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

180
    /**
181
     * files referenced by import statements
182
     */
183
    public get ownScriptImports() {
184
        const result = this.cache?.getOrAdd('BrsFile_ownScriptImports', () => {
3,970!
185
            const result = [] as FileReference[];
3,411✔
186
            for (const statement of this._cachedLookups?.importStatements ?? []) {
3,411!
187
                //register import statements
188
                if (isImportStatement(statement) && statement.tokens.path) {
382✔
189
                    result.push({
380✔
190
                        filePathRange: statement.tokens.path.location?.range,
1,140✔
191
                        destPath: util.getPkgPathFromTarget(this.destPath, statement.filePath),
192
                        sourceFile: this,
193
                        text: statement.tokens.path.text
194
                    });
195
                }
196
            }
197
            return result;
3,411✔
198
        }) ?? [];
3,970!
199
        return result;
3,970✔
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) {
652✔
208
            return this._needsTranspiled;
2✔
209
        }
210
        return !!(this.extension?.endsWith('.bs') || this.program?.options?.allowBrighterScriptInBrightScript || this.editor?.hasChanges);
650!
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;
11,893!
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) {
238✔
229
            if (util.rangeContains(token.location?.range, position)) {
3,553!
230
                return token;
210✔
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) {
114✔
240
            if (util.comparePositionToRange(position, token.location?.range) < 0) {
1,629!
241
                return token;
113✔
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
        const handle = new CancellationTokenSource();
213✔
251
        let containingNode: AstNode;
252
        this.ast.walk((node) => {
213✔
253
            const latestContainer = containingNode;
3,251✔
254
            //bsc walks depth-first
255
            if (util.rangeContains(node.location?.range, position)) {
3,251!
256
                containingNode = node;
963✔
257
            }
258
            //we had a match before, and don't now. this means we've finished walking down the whole way, and found our match
259
            if (latestContainer && !containingNode) {
3,251!
260
                containingNode = latestContainer;
×
261
                handle.cancel();
×
262
            }
263
        }, {
264
            walkMode: WalkMode.visitAllRecursive,
265
            cancel: handle.token
266
        });
267
        return containingNode;
213✔
268
    }
269

270
    public get parser() {
271
        if (!this._parser) {
31,445✔
272
            //remove the typedef file (if it exists)
273
            this.hasTypedef = false;
5✔
274
            this.typedefFile = undefined;
5✔
275

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

279
            //re-link the typedef (if it exists...which it should)
280
            this.resolveTypedef();
5✔
281
        }
282
        return this._parser;
31,445✔
283
    }
284
    private _parser: Parser;
285

286
    public fileContents: string;
287

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

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

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

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

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

317
    public onDependenciesChanged(event: DependencyChangedEvent) {
318
        this.resolveTypedef();
2,285✔
319
    }
320

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

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

334
        //if this is a .brs file, watch for typedef changes
335
        if (this.extension === '.brs') {
1,882✔
336
            result.push(
457✔
337
                util.getTypedefPath(this.destPath)
338
            );
339
        }
340
        return result;
1,882✔
341
    }
342

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

350
        try {
2,225✔
351
            this.fileContents = fileContents;
2,225✔
352

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

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

368
            this.getCommentFlags(lexer.tokens);
2,217✔
369

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

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

385

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

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

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

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

430
    private _propertyNameCompletions: CompletionItem[];
431

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

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

446
        this.commentFlags = [];
2,217✔
447
        for (let lexerToken of tokens) {
2,217✔
448
            for (let triviaToken of lexerToken.leadingTrivia ?? []) {
97,514!
449
                if (triviaToken.kind === TokenKind.Comment) {
81,117✔
450
                    processor.tryAdd(triviaToken.text, triviaToken.location?.range);
5,289!
451
                }
452
            }
453
        }
454
        this.commentFlags.push(...processor.commentFlags);
2,217✔
455
        this.program?.diagnostics.register(processor.diagnostics);
2,217!
456
    }
457

458
    public scopesByFunc = new Map<FunctionExpression, FunctionScope>();
2,492✔
459

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

467
        //create a functionScope for every function
468
        this._functionScopes = [];
77✔
469

470
        for (let func of functions) {
77✔
471
            let scope = new FunctionScope(func);
95✔
472

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

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

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

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

521
            this.scopesByFunc.set(func, scope);
95✔
522

523
            //find every statement in the scope
524
            this._functionScopes.push(scope);
95✔
525
        }
526

527
        //find every variable assignment in the whole file
528
        let assignmentStatements = this._cachedLookups.assignmentStatements;
77✔
529

530
        for (let statement of assignmentStatements) {
77✔
531

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

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

552
    public staticCallables: Callable[];
553

554
    get callables(): Callable[] {
555

556
        if (this.staticCallables) {
5,490✔
557
            // globalFile can statically set the callables
558
            return this.staticCallables;
1,617✔
559
        }
560

561
        return this.cache.getOrAdd(`BrsFile_callables`, () => {
3,873✔
562
            const callables = [];
1,547✔
563

564
            for (let statement of this._cachedLookups.functionStatements ?? []) {
1,547!
565

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

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

594
            return callables;
1,547✔
595
        });
596

597

598
    }
599

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

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

627
    /**
628
     * Find the NamespaceStatement enclosing the given position
629
     */
630
    public getNamespaceStatementForPosition(position: Position): NamespaceStatement {
631
        if (position) {
10,066✔
632
            return this.cache.getOrAdd(`namespaceStatementForPosition-${position.line}:${position.character}`, () => {
10,065✔
633
                for (const statement of this._cachedLookups.namespaceStatements) {
8,786✔
634
                    if (util.rangeContains(statement.location?.range, position)) {
131!
635
                        return statement;
91✔
636
                    }
637
                }
638
            });
639
        }
640
    }
641

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

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

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

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

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

691
    public getTokenBefore(currentToken: Token, tokenKind?: TokenKind): Token {
692
        const index = this.parser.tokens.indexOf(currentToken);
187✔
693
        if (!tokenKind) {
187✔
694
            return this.parser.tokens[index - 1];
160✔
695
        }
696
        for (let i = index - 1; i >= 0; i--) {
27✔
697
            currentToken = this.parser.tokens[i];
42✔
698
            if (currentToken.kind === TokenKind.Newline) {
42✔
699
                break;
6✔
700
            } else if (currentToken.kind === tokenKind) {
36✔
701
                return currentToken;
21✔
702
            }
703
        }
704
        return undefined;
6✔
705
    }
706

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

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

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

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

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

753
        if (isVariableExpression(left)) {
876✔
754
            const leftType = left.getType({ flags: SymbolTypeFlag.runtime });
793✔
755
            if (isNamespaceType(leftType)) {
793✔
756
                let lowerName = left.tokens.name.text.toLowerCase();
9✔
757
                //find the first scope that contains this namespace
758
                let scopes = this.program.getScopesForFile(this);
9✔
759
                for (let scope of scopes) {
9✔
760
                    if (scope.namespaceLookup.has(lowerName)) {
9✔
761
                        return true;
8✔
762
                    }
763
                }
764
            }
765
        }
766
        return false;
868✔
767
    }
768

769
    /**
770
     * Get the token closest to the position. if no token is found, the previous token is returned
771
     */
772
    public getClosestToken(position: Position) {
773
        let tokens = this.parser.tokens;
108✔
774
        for (let i = 0; i < tokens.length; i++) {
108✔
775
            let token = tokens[i];
1,274✔
776
            if (util.rangeContains(token.location?.range, position)) {
1,274!
777
                return token;
108✔
778
            }
779
            //if the position less than this token range, then this position touches no token,
780
            if (util.positionIsGreaterThanRange(position, token.location?.range) === false) {
1,166!
UNCOV
781
                let t = tokens[i - 1];
×
782
                //return the token or the first token
UNCOV
783
                return t ? t : tokens[0];
×
784
            }
785
        }
786
        //return the last token
UNCOV
787
        return tokens[tokens.length - 1];
×
788
    }
789

790
    /**
791
     * Builds a list of document symbols for this file. Used by LanguageServer's onDocumentSymbol functionality
792
     * @deprecated use `DocumentSymbolProvider.process()` instead
793
     */
794
    public getDocumentSymbols() {
795
        return new DocumentSymbolProcessor({
6✔
796
            documentSymbols: [],
797
            file: this,
798
            program: this.program
799
        }).process();
800
    }
801

802
    /**
803
     * Builds a list of workspace symbols for this file. Used by LanguageServer's onWorkspaceSymbol functionality
804
     */
805
    public getWorkspaceSymbols() {
806
        return new WorkspaceSymbolProcessor({
×
807
            program: this.program,
808
            workspaceSymbols: []
809
        }).process();
810
    }
811

812
    /**
813
     * Given a position in a file, if the position is sitting on some type of identifier,
814
     * go to the definition of that identifier (where this thing was first defined)
815
     * @deprecated use `DefinitionProvider.process()` instead
816
     */
817
    public getDefinition(position: Position): Location[] {
818
        return new DefinitionProvider({
×
819
            program: this.program,
820
            file: this,
821
            position: position,
822
            definitions: []
823
        }).process();
824
    }
825

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

846
        return results;
2✔
847
    }
848

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

873
        }
874
        return statement;
132✔
875
    }
876

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

891
    /**
892
     * Generate the code, map, and typedef for this file
893
     */
894
    public serialize(): SerializedCodeFile {
895
        const result: SerializedCodeFile = {};
643✔
896

897
        const transpiled = this.transpile();
643✔
898
        if (typeof transpiled.code === 'string') {
643!
899
            result.code = transpiled.code;
643✔
900
        }
901
        if (transpiled.map) {
643✔
902
            result.map = transpiled.map.toString();
335✔
903
        }
904
        //generate the typedef (if this is not a typedef itself, and if enabled)
905
        if (!this.isTypedef && this.program.options.emitDefinitions) {
643✔
906
            result.typedef = this.getTypedef();
21✔
907
        }
908
        return result;
643✔
909
    }
910

911
    /**
912
     * Convert the brightscript/brighterscript source code into valid brightscript
913
     */
914
    public transpile(): CodeWithSourceMap {
915
        const state = new BrsTranspileState(this);
650✔
916
        state.editor = this.editor ?? new Editor();
650✔
917
        let transpileResult: SourceNode | undefined;
918

919
        if (this.needsTranspiled) {
650✔
920
            const astTranspile = this.ast.transpile(state);
635✔
921
            const trailingComments = [];
635✔
922
            if (util.hasLeadingComments(this.parser.eofToken)) {
635✔
923
                if (util.isLeadingCommentOnSameLine(this.ast.statements[this.ast.statements.length - 1]?.location, this.parser.eofToken)) {
4!
924
                    trailingComments.push(' ');
2✔
925
                } else {
926
                    trailingComments.push('\n');
2✔
927
                }
928
                trailingComments.push(...state.transpileLeadingComments(this.parser.eofToken));
4✔
929
            }
930

931
            transpileResult = util.sourceNodeFromTranspileResult(null, null, state.srcPath, [...astTranspile, ...trailingComments]);
635✔
932
        } else if (this.program.options.sourceMap) {
15✔
933
            //emit code as-is with a simple map to the original file location
934
            transpileResult = util.simpleMap(state.srcPath, this.fileContents);
4✔
935
        } else {
936
            //simple SourceNode wrapping the entire file to simplify the logic below
937
            transpileResult = new SourceNode(null, null, state.srcPath, this.fileContents);
11✔
938
        }
939

940
        //if we created an editor for this flow, undo the edits now
941
        if (!this.editor) {
650✔
942
            //undo any AST edits that the transpile cycle has made
943
            state.editor.undoAll();
7✔
944
        }
945

946
        if (this.program.options.sourceMap) {
650✔
947
            const stagingFileName = path.basename(state.srcPath).replace(/\.bs$/, '.brs');
342✔
948
            return new SourceNode(null, null, stagingFileName, [
342✔
949
                transpileResult,
950
                //add the sourcemap reference comment
951
                state.newline + `'//# sourceMappingURL=./${stagingFileName}.map`
952
            ]).toStringWithSourceMap({ file: stagingFileName });
953
        } else {
954
            return {
308✔
955
                code: transpileResult.toString(),
956
                map: undefined
957
            };
958
        }
959
    }
960

961
    public validationSegmenter = new AstValidationSegmenter(this);
2,492✔
962

963
    public getNamespaceSymbolTable(allowCache = true) {
4,690✔
964
        if (!allowCache) {
6,290✔
965
            return this.constructNamespaceSymbolTable();
1,600✔
966
        }
967
        return this.cache?.getOrAdd(`namespaceSymbolTable`, () => this.constructNamespaceSymbolTable());
4,690!
968
    }
969

970
    private constructNamespaceSymbolTable() {
971
        const nsTable = new SymbolTable(`File NamespaceTypes ${this.destPath}`, () => this.program?.globalScope.symbolTable);
5,891!
972
        this.populateNameSpaceSymbolTable(nsTable);
3,172✔
973
        return nsTable;
3,172✔
974
    }
975

976
    public processSymbolInformation() {
977
        // Get namespaces across imported files
978
        const nsTable = this.getNamespaceSymbolTable(false);
1,600✔
979
        this.linkSymbolTableDisposables.push(this.ast.symbolTable.addSibling(nsTable));
1,600✔
980

981
        this.validationSegmenter.processTree(this.ast);
1,600✔
982
        this.program.addFileSymbolInfo(this);
1,600✔
983
        this.unlinkNamespaceSymbolTables();
1,600✔
984
    }
985

986
    public unlinkNamespaceSymbolTables() {
987
        for (let disposable of this.linkSymbolTableDisposables) {
1,600✔
988
            disposable();
1,600✔
989
        }
990
        this.linkSymbolTableDisposables = [];
1,600✔
991
    }
992

993
    private linkSymbolTableDisposables = [];
2,492✔
994

995

996
    public populateNameSpaceSymbolTable(namespaceSymbolTable: SymbolTable) {
997
        //Add namespace aggregates to namespace member tables
998
        const namespaceTypesKnown = new Map<string, BscType>();
3,172✔
999
        // eslint-disable-next-line no-bitwise
1000
        let getTypeOptions = { flags: SymbolTypeFlag.runtime | SymbolTypeFlag.typetime };
3,172✔
1001
        for (const [nsName, nsContainer] of this.getNamespaceLookupObject()) {
3,172✔
1002
            let currentNSType: BscType = null;
1,565✔
1003
            let parentNSType: BscType = null;
1,565✔
1004
            const existingNsStmt = nsContainer.namespaceStatements?.[0];
1,565!
1005

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

1042
    public get requiredSymbols() {
1043
        return this.cache.getOrAdd(`requiredSymbols`, () => {
5,347✔
1044
            const allNeededSymbolSets = this.validationSegmenter.unresolvedSegmentsSymbols.values();
1,605✔
1045

1046
            const requiredSymbols: UnresolvedSymbol[] = [];
1,605✔
1047
            const addedSymbols = new Map<SymbolTypeFlag, Set<string>>();
1,605✔
1048
            addedSymbols.set(SymbolTypeFlag.runtime, new Set());
1,605✔
1049
            addedSymbols.set(SymbolTypeFlag.typetime, new Set());
1,605✔
1050
            for (const setOfSymbols of allNeededSymbolSets) {
1,605✔
1051
                for (const symbol of setOfSymbols) {
405✔
1052
                    const fullSymbolKey = symbol.typeChain.map(tce => tce.name).join('.').toLowerCase();
872✔
1053
                    const flag = symbol.endChainFlags;
474✔
1054
                    if (this.providedSymbols.symbolMap.get(flag)?.has(fullSymbolKey)) {
474!
1055
                        // this catches namespaced things
NEW
1056
                        continue;
×
1057
                    }
1058
                    if (this.ast.getSymbolTable().hasSymbol(fullSymbolKey, flag)) {
474✔
1059
                        //catches aliases
1060
                        continue;
7✔
1061
                    }
1062
                    if (!addedSymbols.get(flag)?.has(fullSymbolKey)) {
467!
1063
                        requiredSymbols.push(symbol);
440✔
1064
                        addedSymbols.get(flag)?.add(fullSymbolKey);
440!
1065
                    }
1066
                }
1067
            }
1068
            return requiredSymbols;
1,605✔
1069
        });
1070
    }
1071

1072
    public get providedSymbols() {
1073
        return this.cache?.getOrAdd(`providedSymbols`, () => {
7,955!
1074
            return this.getProvidedSymbols();
1,603✔
1075
        });
1076
    }
1077

1078
    public get assignedSymbols() {
NEW
1079
        return this.cache.getOrAdd(`assignedSymbols`, () => {
×
NEW
1080
            const allAssignedSymbolsEntries = this.validationSegmenter.assignedTokensInSegment.entries() ?? [];
×
1081

NEW
1082
            let allAssignedSymbolsSet: AssignedSymbol[] = [];
×
1083

NEW
1084
            for (const [_segment, assignedSymbolSet] of allAssignedSymbolsEntries) {
×
NEW
1085
                allAssignedSymbolsSet.push(...assignedSymbolSet.values());
×
1086
            }
NEW
1087
            return allAssignedSymbolsSet;
×
1088
        });
1089
    }
1090

1091
    private getProvidedSymbols() {
1092
        const symbolMap = new Map<SymbolTypeFlag, Map<string, ProvidedSymbol>>();
1,603✔
1093
        const runTimeSymbolMap = new Map<string, ProvidedSymbol>();
1,603✔
1094
        const typeTimeSymbolMap = new Map<string, ProvidedSymbol>();
1,603✔
1095
        const referenceSymbolMap = new Map<SymbolTypeFlag, Map<string, ProvidedSymbol>>();
1,603✔
1096
        const referenceRunTimeSymbolMap = new Map<string, ProvidedSymbol>();
1,603✔
1097
        const referenceTypeTimeSymbolMap = new Map<string, ProvidedSymbol>();
1,603✔
1098

1099
        const tablesToGetSymbolsFrom: Array<{ table: SymbolTable; namePrefixLower?: string }> = [{
1,603✔
1100
            table: this.parser.symbolTable
1101
        }];
1102

1103
        for (const namespaceStatement of this._cachedLookups.namespaceStatements) {
1,603✔
1104
            tablesToGetSymbolsFrom.push({
563✔
1105
                table: namespaceStatement.body.getSymbolTable(),
1106
                namePrefixLower: namespaceStatement.getName(ParseMode.BrighterScript).toLowerCase()
1107
            });
1108
        }
1109

1110
        function getAnyDuplicates(symbolNameLower: string, providedSymbolMap: Map<string, ProvidedSymbol>, referenceProvidedSymbolMap: Map<string, ProvidedSymbol>) {
1111
            if (symbolNameLower === 'm') {
3,440!
NEW
1112
                return [];
×
1113
            }
1114
            let duplicates = [] as Array<BscSymbol>;
3,440✔
1115
            let existingSymbol = providedSymbolMap.get(symbolNameLower);
3,440✔
1116
            if (existingSymbol) {
3,440✔
1117
                duplicates.push(existingSymbol.symbol, ...existingSymbol.duplicates);
17✔
1118
            }
1119
            existingSymbol = referenceProvidedSymbolMap.get(symbolNameLower);
3,440✔
1120
            if (existingSymbol) {
3,440!
NEW
1121
                duplicates.push(existingSymbol.symbol, ...existingSymbol.duplicates);
×
1122
            }
1123

1124
            return duplicates;
3,440✔
1125
        }
1126

1127
        for (const symbolTable of tablesToGetSymbolsFrom) {
1,603✔
1128
            const runTimeSymbols = symbolTable.table.getOwnSymbols(SymbolTypeFlag.runtime);
2,166✔
1129
            const typeTimeSymbols = symbolTable.table.getOwnSymbols(SymbolTypeFlag.typetime);
2,166✔
1130

1131
            for (const symbol of runTimeSymbols) {
2,166✔
1132
                const symbolNameLower = symbolTable.namePrefixLower
2,769✔
1133
                    ? `${symbolTable.namePrefixLower}.${symbol.name.toLowerCase()}`
2,769✔
1134
                    : symbol.name.toLowerCase();
1135
                if (symbolNameLower === 'm') {
2,769✔
1136
                    continue;
8✔
1137
                }
1138
                const duplicates = getAnyDuplicates(symbolNameLower, runTimeSymbolMap, referenceRunTimeSymbolMap);
2,761✔
1139

1140
                if (!isAnyReferenceType(symbol.type)) {
2,761✔
1141
                    runTimeSymbolMap.set(symbolNameLower, { symbol: symbol, duplicates: duplicates });
2,725✔
1142
                } else {
1143
                    referenceRunTimeSymbolMap.set(symbolNameLower, { symbol: symbol, duplicates: duplicates });
36✔
1144
                }
1145
            }
1146

1147
            for (const symbol of typeTimeSymbols) {
2,166✔
1148
                const symbolNameLower = symbolTable.namePrefixLower
679✔
1149
                    ? `${symbolTable.namePrefixLower}.${symbol.name.toLowerCase()}`
679✔
1150
                    : symbol.name.toLowerCase();
1151
                if (symbolNameLower === 'm') {
679!
NEW
1152
                    continue;
×
1153
                }
1154
                const duplicates = getAnyDuplicates(symbolNameLower, typeTimeSymbolMap, referenceTypeTimeSymbolMap);
679✔
1155
                if (!isAnyReferenceType(symbol.type)) {
679✔
1156
                    typeTimeSymbolMap.set(symbolNameLower, { symbol: symbol, duplicates: duplicates });
655✔
1157
                } else {
1158
                    referenceTypeTimeSymbolMap.set(symbolNameLower, { symbol: symbol, duplicates: duplicates });
24✔
1159
                }
1160
            }
1161
        }
1162

1163
        symbolMap.set(SymbolTypeFlag.runtime, runTimeSymbolMap);
1,603✔
1164
        symbolMap.set(SymbolTypeFlag.typetime, typeTimeSymbolMap);
1,603✔
1165

1166
        referenceSymbolMap.set(SymbolTypeFlag.runtime, referenceRunTimeSymbolMap);
1,603✔
1167
        referenceSymbolMap.set(SymbolTypeFlag.typetime, referenceTypeTimeSymbolMap);
1,603✔
1168

1169
        const changes = new Map<SymbolTypeFlag, Set<string>>();
1,603✔
1170
        changes.set(SymbolTypeFlag.runtime, new Set<string>());
1,603✔
1171
        changes.set(SymbolTypeFlag.typetime, new Set<string>());
1,603✔
1172
        const previouslyProvidedSymbols = this.program.getFileSymbolInfo(this)?.provides.symbolMap;
1,603✔
1173
        const previousSymbolsChecked = new Map<SymbolTypeFlag, Set<string>>();
1,603✔
1174
        previousSymbolsChecked.set(SymbolTypeFlag.runtime, new Set<string>());
1,603✔
1175
        previousSymbolsChecked.set(SymbolTypeFlag.typetime, new Set<string>());
1,603✔
1176
        for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
1,603✔
1177
            const newSymbolMapForFlag = symbolMap.get(flag);
3,206✔
1178
            const oldSymbolMapForFlag = previouslyProvidedSymbols?.get(flag);
3,206✔
1179
            const previousSymbolsCheckedForFlag = previousSymbolsChecked.get(flag);
3,206✔
1180
            const changesForFlag = changes.get(flag);
3,206✔
1181
            if (!oldSymbolMapForFlag) {
3,206✔
1182
                for (const key of newSymbolMapForFlag.keys()) {
3,008✔
1183
                    changesForFlag.add(key);
3,193✔
1184
                }
1185
                continue;
3,008✔
1186

1187
            }
1188
            for (const [symbolKey, symbolObj] of newSymbolMapForFlag) {
198✔
1189
                const symbolType = symbolObj.symbol.type;
170✔
1190
                const previousType = oldSymbolMapForFlag?.get(symbolKey)?.symbol?.type;
170!
1191
                previousSymbolsCheckedForFlag.add(symbolKey);
170✔
1192
                if (!previousType) {
170✔
1193
                    changesForFlag.add(symbolKey);
28✔
1194
                    continue;
28✔
1195
                }
1196
                const data = {};
142✔
1197
                if (!symbolType.isEqual(previousType, data)) {
142✔
1198
                    changesForFlag.add(symbolKey);
53✔
1199
                }
1200
            }
1201
            for (const [symbolKey] of previouslyProvidedSymbols.get(flag)) {
198✔
1202
                if (!previousSymbolsCheckedForFlag.has(symbolKey)) {
164✔
1203
                    changesForFlag.add(symbolKey);
22✔
1204
                }
1205
            }
1206
        }
1207
        return {
1,603✔
1208
            symbolMap: symbolMap,
1209
            changes: changes,
1210
            referenceSymbolMap: referenceSymbolMap
1211
        };
1212
    }
1213

1214
    public markSegmentAsValidated(node: AstNode) {
1215
        this.validationSegmenter.markSegmentAsValidated(node);
3,008✔
1216
    }
1217

1218
    public getNamespaceLookupObject() {
1219
        if (!this.isValidated) {
6,985✔
1220
            return this.buildNamespaceLookup();
196✔
1221
        }
1222
        return this.cache.getOrAdd(`namespaceLookup`, () => {
6,789✔
1223
            const nsLookup = this.buildNamespaceLookup();
1,543✔
1224
            return nsLookup;
1,543✔
1225
        });
1226
    }
1227

1228
    private buildNamespaceLookup() {
1229
        const namespaceLookup = new Map<string, NamespaceContainer>();
1,739✔
1230
        for (let namespaceStatement of this._cachedLookups.namespaceStatements) {
1,739✔
1231
            let nameParts = namespaceStatement.getNameParts();
580✔
1232

1233
            let loopName: string = null;
580✔
1234
            let lowerLoopName: string = null;
580✔
1235
            let parentNameLower: string = null;
580✔
1236

1237
            //ensure each namespace section is represented in the results
1238
            //(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"
1239
            for (let i = 0; i < nameParts.length; i++) {
580✔
1240

1241
                let part = nameParts[i];
939✔
1242
                let lowerPartName = part.text.toLowerCase();
939✔
1243

1244
                if (i === 0) {
939✔
1245
                    loopName = part.text;
580✔
1246
                    lowerLoopName = lowerPartName;
580✔
1247
                } else {
1248
                    parentNameLower = lowerLoopName;
359✔
1249
                    loopName += '.' + part.text;
359✔
1250
                    lowerLoopName += '.' + lowerPartName;
359✔
1251
                }
1252
                if (!namespaceLookup.has(lowerLoopName)) {
939✔
1253
                    namespaceLookup.set(lowerLoopName, {
823✔
1254
                        isTopLevel: i === 0,
1255
                        file: this,
1256
                        fullName: loopName,
1257
                        fullNameLower: lowerLoopName,
1258
                        parentNameLower: parentNameLower,
1259
                        nameParts: nameParts.slice(0, i),
1260
                        nameRange: namespaceStatement.nameExpression.location?.range,
2,469✔
1261
                        lastPartName: part.text,
1262
                        lastPartNameLower: lowerPartName,
1263
                        functionStatements: new Map(),
1264
                        namespaceStatements: [],
1265
                        namespaces: new Map(),
1266
                        classStatements: new Map(),
1267
                        enumStatements: new Map(),
1268
                        constStatements: new Map(),
1269
                        statements: [],
1270
                        // the aggregate symbol table should have no parent. It should include just the symbols of the namespace.
1271
                        symbolTable: new SymbolTable(`Namespace Aggregate: '${loopName}'`)
1272
                    });
1273
                }
1274
            }
1275
            let ns = namespaceLookup.get(lowerLoopName);
580✔
1276
            ns.namespaceStatements.push(namespaceStatement);
580✔
1277
            ns.statements.push(...namespaceStatement.body.statements);
580✔
1278
            for (let statement of namespaceStatement.body.statements) {
580✔
1279
                if (isClassStatement(statement) && statement.tokens.name) {
706✔
1280
                    ns.classStatements.set(statement.tokens.name.text.toLowerCase(), statement);
126✔
1281
                } else if (isFunctionStatement(statement) && statement.tokens.name) {
580✔
1282
                    ns.functionStatements.set(statement.tokens.name.text.toLowerCase(), statement);
387✔
1283
                } else if (isEnumStatement(statement) && statement.fullName) {
193✔
1284
                    ns.enumStatements.set(statement.fullName.toLowerCase(), statement);
38✔
1285
                } else if (isConstStatement(statement) && statement.fullName) {
155✔
1286
                    ns.constStatements.set(statement.fullName.toLowerCase(), statement);
94✔
1287
                }
1288
            }
1289
            // Merges all the symbol tables of the namespace statements into the new symbol table created above.
1290
            // Set those symbol tables to have this new merged table as a parent
1291
            ns.symbolTable.mergeSymbolTable(namespaceStatement.body.getSymbolTable());
580✔
1292
        }
1293
        return namespaceLookup;
1,739✔
1294
    }
1295

1296
    public getTypedef() {
1297
        const state = new BrsTranspileState(this);
34✔
1298
        const typedef = this.ast.getTypedef(state);
34✔
1299
        const programNode = util.sourceNodeFromTranspileResult(null, null, this.srcPath, typedef);
34✔
1300
        return programNode.toString();
34✔
1301
    }
1302

1303
    public dispose() {
1304
        this._parser?.dispose();
1,740✔
1305

1306
        //deleting these properties result in lower memory usage (garbage collection is magic!)
1307
        delete this.fileContents;
1,740✔
1308
        delete this._parser;
1,740✔
1309
        delete this._functionScopes;
1,740✔
1310
        delete this.scopesByFunc;
1,740✔
1311
    }
1312
}
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