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

rokucommunity / brighterscript / #14295

24 Apr 2025 05:48PM UTC coverage: 87.099% (-0.006%) from 87.105%
#14295

push

web-flow
Merge d462d6d77 into 382146cf4

13489 of 16372 branches covered (82.39%)

Branch coverage included in aggregate %.

14462 of 15719 relevant lines covered (92.0%)

19990.89 hits per line

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

85.4
/src/util.ts
1
import * as fs from 'fs';
1✔
2
import * as fsExtra from 'fs-extra';
1✔
3
import type { ParseError } from 'jsonc-parser';
4
import { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser';
1✔
5
import * as path from 'path';
1✔
6
import { rokuDeploy, DefaultFiles } from 'roku-deploy';
1✔
7
import type { Diagnostic, Position, DiagnosticRelatedInformation } from 'vscode-languageserver';
8
import { Range, Location } from 'vscode-languageserver';
1✔
9
import { URI } from 'vscode-uri';
1✔
10
import * as xml2js from 'xml2js';
1✔
11
import type { BsConfig, FinalizedBsConfig } from './BsConfig';
12
import { DiagnosticMessages } from './DiagnosticMessages';
1✔
13
import type { CallableContainer, BsDiagnostic, FileReference, CallableContainerMap, Plugin, ExpressionInfo, TranspileResult, MaybePromise, DisposableLike, ExtraSymbolData, GetTypeOptions, TypeChainProcessResult, PluginFactory } from './interfaces';
14
import { TypeChainEntry } from './interfaces';
1✔
15
import { BooleanType } from './types/BooleanType';
1✔
16
import { DoubleType } from './types/DoubleType';
1✔
17
import { DynamicType } from './types/DynamicType';
1✔
18
import { FloatType } from './types/FloatType';
1✔
19
import { IntegerType } from './types/IntegerType';
1✔
20
import { LongIntegerType } from './types/LongIntegerType';
1✔
21
import { ObjectType } from './types/ObjectType';
1✔
22
import { StringType } from './types/StringType';
1✔
23
import { VoidType } from './types/VoidType';
1✔
24
import { ParseMode } from './parser/Parser';
1✔
25
import type { CallExpression, CallfuncExpression, DottedGetExpression, FunctionParameterExpression, IndexedGetExpression, LiteralExpression, NewExpression, TypeExpression, VariableExpression, XmlAttributeGetExpression } from './parser/Expression';
26
import { LogLevel, createLogger } from './logging';
1✔
27
import { isToken, type Identifier, type Locatable, type Token } from './lexer/Token';
1✔
28
import { TokenKind } from './lexer/TokenKind';
1✔
29
import { isAnyReferenceType, isBinaryExpression, isBooleanTypeLike, isBrsFile, isCallExpression, isCallableType, isCallfuncExpression, isClassType, isComponentType, isDottedGetExpression, isDoubleTypeLike, isDynamicType, isEnumMemberType, isExpression, isFloatTypeLike, isIndexedGetExpression, isIntegerTypeLike, isInvalidTypeLike, isLiteralString, isLongIntegerTypeLike, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberType, isObjectType, isPrimitiveType, isReferenceType, isStatement, isStringTypeLike, isTypeExpression, isTypedArrayExpression, isTypedFunctionType, isUninitializedType, isUnionType, isVariableExpression, isVoidType, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection';
1✔
30
import { WalkMode } from './astUtils/visitors';
1✔
31
import { SourceNode } from 'source-map';
1✔
32
import * as requireRelative from 'require-relative';
1✔
33
import type { BrsFile } from './files/BrsFile';
34
import type { XmlFile } from './files/XmlFile';
35
import type { AstNode, Expression, Statement } from './parser/AstNode';
36
import { AstNodeKind } from './parser/AstNode';
1✔
37
import type { UnresolvedSymbol } from './AstValidationSegmenter';
38
import type { BscSymbol, GetSymbolTypeOptions, SymbolTable } from './SymbolTable';
39
import { SymbolTypeFlag } from './SymbolTypeFlag';
1✔
40
import { createIdentifier, createToken } from './astUtils/creators';
1✔
41
import { MAX_RELATED_INFOS_COUNT } from './diagnosticUtils';
1✔
42
import type { BscType } from './types/BscType';
43
import { unionTypeFactory } from './types/UnionType';
1✔
44
import { ArrayType } from './types/ArrayType';
1✔
45
import { BinaryOperatorReferenceType, TypePropertyReferenceType, ParamTypeFromValueReferenceType } from './types/ReferenceType';
1✔
46
import { AssociativeArrayType } from './types/AssociativeArrayType';
1✔
47
import { ComponentType } from './types/ComponentType';
1✔
48
import { FunctionType } from './types/FunctionType';
1✔
49
import type { AssignmentStatement, NamespaceStatement } from './parser/Statement';
50
import type { BscFile } from './files/BscFile';
51
import type { NamespaceType } from './types/NamespaceType';
52
import { getUniqueType } from './types/helpers';
1✔
53
import { InvalidType } from './types/InvalidType';
1✔
54

55
export class Util {
1✔
56
    public clearConsole() {
57
        // process.stdout.write('\x1Bc');
58
    }
59

60
    /**
61
     * Get the version of brighterscript
62
     */
63
    public getBrighterScriptVersion() {
64
        try {
7✔
65
            return fsExtra.readJsonSync(`${__dirname}/../package.json`).version;
7✔
66
        } catch {
67
            return undefined;
×
68
        }
69
    }
70

71
    /**
72
     * Returns the number of parent directories in the filPath
73
     */
74
    public getParentDirectoryCount(filePath: string | undefined) {
75
        if (!filePath) {
1,933!
76
            return -1;
×
77
        } else {
78
            return filePath.replace(/^pkg:/, '').split(/[\\\/]/).length - 1;
1,933✔
79
        }
80
    }
81

82
    /**
83
     * Determine if the file exists
84
     */
85
    public async pathExists(filePath: string | undefined) {
86
        if (!filePath) {
190✔
87
            return false;
1✔
88
        } else {
89
            return fsExtra.pathExists(filePath);
189✔
90
        }
91
    }
92

93
    /**
94
     * Determine if the file exists
95
     */
96
    public pathExistsSync(filePath: string | undefined) {
97
        if (!filePath) {
8,607!
98
            return false;
×
99
        } else {
100
            return fsExtra.pathExistsSync(filePath);
8,607✔
101
        }
102
    }
103

104
    /**
105
     * Determine if this path is a directory
106
     */
107
    public isDirectorySync(dirPath: string | undefined) {
108
        try {
80✔
109
            return dirPath !== undefined && fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
80✔
110
        } catch (e) {
111
            return false;
×
112
        }
113
    }
114

115
    /**
116
     * Read a file from disk. If a failure occurrs, simply return undefined
117
     * @param filePath path to the file
118
     * @returns the string contents, or undefined if the file doesn't exist
119
     */
120
    public readFileSync(filePath: string): Buffer | undefined {
121
        try {
11✔
122
            return fsExtra.readFileSync(filePath);
11✔
123
        } catch (e) {
124
            return undefined;
2✔
125
        }
126
    }
127

128
    /**
129
     * Given a pkg path of any kind, transform it to a roku-specific pkg path (i.e. "pkg:/some/path.brs")
130
     */
131
    public sanitizePkgPath(pkgPath: string) {
132
        //convert all slashes to forwardslash
133
        pkgPath = pkgPath.replace(/[\/\\]+/g, '/');
49✔
134
        //ensure every path has the leading pkg:/
135
        return 'pkg:/' + pkgPath.replace(/^pkg:\//i, '');
49✔
136
    }
137

138
    /**
139
     * Determine if the given path starts with a protocol
140
     */
141
    public startsWithProtocol(path: string) {
142
        return !!/^[-a-z]+:\//i.exec(path);
×
143
    }
144

145
    /**
146
     * Given a pkg path of any kind, transform it to a roku-specific pkg path (i.e. "pkg:/some/path.brs")
147
     * @deprecated use `sanitizePkgPath instead. Will be removed in v1
148
     */
149
    public getRokuPkgPath(pkgPath: string) {
150
        return this.sanitizePkgPath(pkgPath);
×
151
    }
152

153
    /**
154
     * Given a path to a file/directory, replace all path separators with the current system's version.
155
     */
156
    public pathSepNormalize(filePath: string, separator?: string) {
157
        if (!filePath) {
14!
158
            return filePath;
×
159
        }
160
        separator = separator ? separator : path.sep;
14!
161
        return filePath.replace(/[\\/]+/g, separator);
14✔
162
    }
163

164
    /**
165
     * Find the path to the config file.
166
     * If the config file path doesn't exist
167
     * @param cwd the current working directory where the search for configs should begin
168
     */
169
    public getConfigFilePath(cwd?: string) {
170
        cwd = cwd ?? process.cwd();
92✔
171
        let configPath = path.join(cwd, 'bsconfig.json');
92✔
172
        //find the nearest config file path
173
        for (let i = 0; i < 100; i++) {
92✔
174
            if (this.pathExistsSync(configPath)) {
8,607✔
175
                return configPath;
6✔
176
            } else {
177
                let parentDirPath = path.dirname(path.dirname(configPath));
8,601✔
178
                configPath = path.join(parentDirPath, 'bsconfig.json');
8,601✔
179
            }
180
        }
181
    }
182

183
    public getRangeFromOffsetLength(text: string, offset: number, length: number) {
184
        let lineIndex = 0;
2✔
185
        let colIndex = 0;
2✔
186
        for (let i = 0; i < text.length; i++) {
2✔
187
            if (offset === i) {
158!
188
                break;
×
189
            }
190
            let char = text[i];
158✔
191
            if (char === '\n' || (char === '\r' && text[i + 1] === '\n')) {
158!
192
                lineIndex++;
4✔
193
                colIndex = 0;
4✔
194
                i++;
4✔
195
                continue;
4✔
196
            } else {
197
                colIndex++;
154✔
198
            }
199
        }
200
        return util.createRange(lineIndex, colIndex, lineIndex, colIndex + length);
2✔
201
    }
202

203
    /**
204
     * Load the contents of a config file.
205
     * If the file extends another config, this will load the base config as well.
206
     * @param configFilePath the relative or absolute path to a brighterscript config json file
207
     * @param parentProjectPaths a list of parent config files. This is used by this method to recursively build the config list
208
     */
209
    public loadConfigFile(configFilePath: string | undefined, parentProjectPaths?: string[], cwd = process.cwd()): BsConfig | undefined {
12✔
210
        if (configFilePath) {
58✔
211
            //if the config file path starts with question mark, then it's optional. return undefined if it doesn't exist
212
            if (configFilePath.startsWith('?')) {
57✔
213
                //remove leading question mark
214
                configFilePath = configFilePath.substring(1);
1✔
215
                if (fsExtra.pathExistsSync(path.resolve(cwd, configFilePath)) === false) {
1!
216
                    return undefined;
1✔
217
                }
218
            }
219
            //keep track of the inheritance chain
220
            parentProjectPaths = parentProjectPaths ? parentProjectPaths : [];
56✔
221
            configFilePath = path.resolve(cwd, configFilePath);
56✔
222
            if (parentProjectPaths?.includes(configFilePath)) {
56!
223
                parentProjectPaths.push(configFilePath);
1✔
224
                parentProjectPaths.reverse();
1✔
225
                throw new Error('Circular dependency detected: "' + parentProjectPaths.join('" => ') + '"');
1✔
226
            }
227
            //load the project file
228
            let projectFileContents = fsExtra.readFileSync(configFilePath).toString();
55✔
229
            let parseErrors = [] as ParseError[];
55✔
230
            let projectConfig = parseJsonc(projectFileContents, parseErrors, {
55✔
231
                allowEmptyContent: true,
232
                allowTrailingComma: true,
233
                disallowComments: false
234
            }) as BsConfig ?? {};
55✔
235
            if (parseErrors.length > 0) {
55✔
236
                let err = parseErrors[0];
2✔
237
                let diagnostic = {
2✔
238
                    ...DiagnosticMessages.syntaxError(`Syntax errors in bsconfig.json: ${printParseErrorCode(parseErrors[0].error)}`),
239
                    location: {
240
                        uri: this.pathToUri(configFilePath),
241
                        range: this.getRangeFromOffsetLength(projectFileContents, err.offset, err.length)
242
                    }
243
                } as BsDiagnostic;
244
                throw diagnostic; //eslint-disable-line @typescript-eslint/no-throw-literal
2✔
245
            }
246

247
            let projectFileCwd = path.dirname(configFilePath);
53✔
248

249
            //`plugins` paths should be relative to the current bsconfig
250
            this.resolvePathsRelativeTo(projectConfig, 'plugins', projectFileCwd);
53✔
251

252
            //`require` paths should be relative to cwd
253
            util.resolvePathsRelativeTo(projectConfig, 'require', projectFileCwd);
53✔
254

255
            let result: BsConfig;
256
            //if the project has a base file, load it
257
            if (projectConfig && typeof projectConfig.extends === 'string') {
53✔
258
                let baseProjectConfig = this.loadConfigFile(projectConfig.extends, [...parentProjectPaths, configFilePath], projectFileCwd);
9✔
259
                //extend the base config with the current project settings
260
                result = { ...baseProjectConfig, ...projectConfig };
7✔
261
            } else {
262
                result = projectConfig;
44✔
263
                let ancestors = parentProjectPaths ? parentProjectPaths : [];
44!
264
                ancestors.push(configFilePath);
44✔
265
                (result as any)._ancestors = parentProjectPaths;
44✔
266
            }
267

268
            //make any paths in the config absolute (relative to the CURRENT config file)
269
            if (result.outFile) {
51✔
270
                result.outFile = path.resolve(projectFileCwd, result.outFile);
4✔
271
            }
272
            if (result.rootDir) {
51✔
273
                result.rootDir = path.resolve(projectFileCwd, result.rootDir);
11✔
274
            }
275
            if (result.cwd) {
51✔
276
                result.cwd = path.resolve(projectFileCwd, result.cwd);
1✔
277
            }
278
            if (result.stagingDir) {
51✔
279
                result.stagingDir = path.resolve(projectFileCwd, result.stagingDir);
2✔
280
            }
281
            if (result.sourceRoot && result.resolveSourceRoot) {
51✔
282
                result.sourceRoot = path.resolve(projectFileCwd, result.sourceRoot);
2✔
283
            }
284
            return result;
51✔
285
        }
286
    }
287

288
    /**
289
     * Convert relative paths to absolute paths, relative to the given directory. Also de-dupes the paths. Modifies the array in-place
290
     * @param collection usually a bsconfig.
291
     * @param key a key of the config to read paths from (usually this is `'plugins'` or `'require'`)
292
     * @param relativeDir the path to the folder where the paths should be resolved relative to. This should be an absolute path
293
     */
294
    public resolvePathsRelativeTo(collection: any, key: string, relativeDir: string) {
295
        if (!collection[key]) {
108✔
296
            return;
104✔
297
        }
298
        const result = new Set<string>();
4✔
299
        for (const p of collection[key] as string[] ?? []) {
4!
300
            if (p) {
12✔
301
                result.add(
11✔
302
                    p?.startsWith('.') ? path.resolve(relativeDir, p) : p
44!
303
                );
304
            }
305
        }
306
        collection[key] = [...result];
4✔
307
    }
308

309
    /**
310
     * Do work within the scope of a changed current working directory
311
     * @param targetCwd the cwd where the work should be performed
312
     * @param callback a function to call when the cwd has been changed to `targetCwd`
313
     */
314
    public cwdWork<T>(targetCwd: string | null | undefined, callback: () => T): T {
315
        let originalCwd = process.cwd();
×
316
        if (targetCwd) {
×
317
            process.chdir(targetCwd);
×
318
        }
319

320
        let result: T;
321
        let err;
322

323
        try {
×
324
            result = callback();
×
325
        } catch (e) {
326
            err = e;
×
327
        }
328

329
        if (targetCwd) {
×
330
            process.chdir(originalCwd);
×
331
        }
332

333
        if (err) {
×
334
            throw err;
×
335
        } else {
336
            //justification: `result` is set as long as `err` is not set and vice versa
337
            return result!;
×
338
        }
339
    }
340

341
    /**
342
     * Given a BsConfig object, start with defaults,
343
     * merge with bsconfig.json and the provided options.
344
     * @param config a bsconfig object to use as the baseline for the resulting config
345
     */
346
    public normalizeAndResolveConfig(config: BsConfig | undefined): FinalizedBsConfig {
347
        let result = this.normalizeConfig({});
127✔
348

349
        if (config?.noProject) {
127✔
350
            return result;
1✔
351
        }
352

353
        //if no options were provided, try to find a bsconfig.json file
354
        if (!config || !config.project) {
126✔
355
            result.project = this.getConfigFilePath(config?.cwd);
87✔
356
        } else {
357
            //use the config's project link
358
            result.project = config.project;
39✔
359
        }
360
        if (result.project) {
126✔
361
            let configFile = this.loadConfigFile(result.project, undefined, config?.cwd);
42!
362
            result = Object.assign(result, configFile);
39✔
363
        }
364
        //override the defaults with the specified options
365
        result = Object.assign(result, config);
123✔
366
        return result;
123✔
367
    }
368

369
    /**
370
     * Set defaults for any missing items
371
     * @param config a bsconfig object to use as the baseline for the resulting config
372
     */
373
    public normalizeConfig(config: BsConfig | undefined): FinalizedBsConfig {
374
        config = config ?? {} as BsConfig;
2,246✔
375

376
        const cwd = config.cwd ?? process.cwd();
2,246✔
377
        const rootFolderName = path.basename(cwd);
2,246✔
378
        const retainStagingDir = (config.retainStagingDir ?? config.retainStagingDir) === true ? true : false;
2,246✔
379

380
        let logLevel: LogLevel = LogLevel.log;
2,246✔
381

382
        if (typeof config.logLevel === 'string') {
2,246✔
383
            logLevel = LogLevel[(config.logLevel as string).toLowerCase()] ?? LogLevel.log;
2!
384
        }
385

386
        let bslibDestinationDir = config.bslibDestinationDir ?? 'source';
2,246✔
387
        if (bslibDestinationDir !== 'source') {
2,246✔
388
            // strip leading and trailing slashes
389
            bslibDestinationDir = bslibDestinationDir.replace(/^(\/*)(.*?)(\/*)$/, '$2');
4✔
390
        }
391

392
        const configWithDefaults: Omit<FinalizedBsConfig, 'rootDir'> = {
2,246✔
393
            cwd: cwd,
394
            deploy: config.deploy === true ? true : false,
2,246!
395
            //use default files array from rokuDeploy
396
            files: config.files ?? [...DefaultFiles],
6,738✔
397
            createPackage: config.createPackage === false ? false : true,
2,246✔
398
            outFile: config.outFile ?? `./out/${rootFolderName}.zip`,
6,738✔
399
            sourceMap: config.sourceMap === true,
400
            username: config.username ?? 'rokudev',
6,738✔
401
            watch: config.watch === true ? true : false,
2,246!
402
            emitFullPaths: config.emitFullPaths === true ? true : false,
2,246!
403
            retainStagingDir: retainStagingDir,
404
            copyToStaging: config.copyToStaging === false ? false : true,
2,246✔
405
            ignoreErrorCodes: config.ignoreErrorCodes ?? [],
6,738✔
406
            diagnosticSeverityOverrides: config.diagnosticSeverityOverrides ?? {},
6,738✔
407
            diagnosticFilters: config.diagnosticFilters ?? [],
6,738✔
408
            diagnosticFiltersV0Compatibility: config.diagnosticFiltersV0Compatibility === true ? true : false,
2,246!
409
            plugins: config.plugins ?? [],
6,738✔
410
            pruneEmptyCodeFiles: config.pruneEmptyCodeFiles === true ? true : false,
2,246✔
411
            autoImportComponentScript: config.autoImportComponentScript === true ? true : false,
2,246✔
412
            showDiagnosticsInConsole: config.showDiagnosticsInConsole === false ? false : true,
2,246✔
413
            sourceRoot: config.sourceRoot ? standardizePath(config.sourceRoot) : undefined,
2,246✔
414
            resolveSourceRoot: config.resolveSourceRoot === true ? true : false,
2,246!
415
            allowBrighterScriptInBrightScript: config.allowBrighterScriptInBrightScript === true ? true : false,
2,246!
416
            emitDefinitions: config.emitDefinitions === true ? true : false,
2,246!
417
            removeParameterTypes: config.removeParameterTypes === true ? true : false,
2,246!
418
            logLevel: logLevel,
419
            bslibDestinationDir: bslibDestinationDir,
420
            legacyCallfuncHandling: config.legacyCallfuncHandling === true ? true : false
2,246!
421
        };
422

423
        //mutate `config` in case anyone is holding a reference to the incomplete one
424
        const merged: FinalizedBsConfig = Object.assign(config, configWithDefaults);
2,246✔
425

426
        return merged;
2,246✔
427
    }
428

429
    /**
430
     * Get the root directory from options.
431
     * Falls back to options.cwd.
432
     * Falls back to process.cwd
433
     * @param options a bsconfig object
434
     */
435
    public getRootDir(options: BsConfig) {
436
        if (!options) {
2,079!
437
            throw new Error('Options is required');
×
438
        }
439
        let cwd = options.cwd;
2,079✔
440
        cwd = cwd ? cwd : process.cwd();
2,079!
441
        let rootDir = options.rootDir ? options.rootDir : cwd;
2,079✔
442

443
        rootDir = path.resolve(cwd, rootDir);
2,079✔
444

445
        return rootDir;
2,079✔
446
    }
447

448
    /**
449
     * Given a list of callables as a dictionary indexed by their full name (namespace included, transpiled to underscore-separated.
450
     */
451
    public getCallableContainersByLowerName(callables: CallableContainer[]): CallableContainerMap {
452
        //find duplicate functions
453
        const result = new Map<string, CallableContainer[]>();
1,894✔
454

455
        for (let callableContainer of callables) {
1,894✔
456
            let lowerName = callableContainer.callable.getName(ParseMode.BrightScript).toLowerCase();
149,820✔
457

458
            //create a new array for this name
459
            const list = result.get(lowerName);
149,820✔
460
            if (list) {
149,820✔
461
                list.push(callableContainer);
7,610✔
462
            } else {
463
                result.set(lowerName, [callableContainer]);
142,210✔
464
            }
465
        }
466
        return result;
1,894✔
467
    }
468

469
    /**
470
     * Split a file by newline characters (LF or CRLF)
471
     */
472
    public getLines(text: string) {
473
        return text.split(/\r?\n/);
×
474
    }
475

476
    /**
477
     * Given an absolute path to a source file, and a target path,
478
     * compute the pkg path for the target relative to the source file's location
479
     */
480
    public getPkgPathFromTarget(containingFilePathAbsolute: string, targetPath: string) {
481
        // https://regex101.com/r/w7CG2N/1
482
        const regexp = /^(?:pkg|libpkg):(\/)?/i;
680✔
483
        const [fullScheme, slash] = regexp.exec(targetPath) ?? [];
680✔
484
        //if the target starts with 'pkg:' or 'libpkg:' then it's an absolute path. Return as is
485
        if (slash) {
680✔
486
            targetPath = targetPath.substring(fullScheme.length);
429✔
487
            if (targetPath === '') {
429✔
488
                return null;
2✔
489
            } else {
490
                return path.normalize(targetPath);
427✔
491
            }
492
        }
493
        //if the path is exactly `pkg:` or `libpkg:`
494
        if (targetPath === fullScheme && !slash) {
251✔
495
            return null;
2✔
496
        }
497

498
        //remove the filename
499
        let containingFolder = path.normalize(path.dirname(containingFilePathAbsolute));
249✔
500
        //start with the containing folder, split by slash
501
        let result = containingFolder.split(path.sep);
249✔
502

503
        //split on slash
504
        let targetParts = path.normalize(targetPath).split(path.sep);
249✔
505

506
        for (let part of targetParts) {
249✔
507
            if (part === '' || part === '.') {
253✔
508
                //do nothing, it means current directory
509
                continue;
4✔
510
            }
511
            if (part === '..') {
249✔
512
                //go up one directory
513
                result.pop();
2✔
514
            } else {
515
                result.push(part);
247✔
516
            }
517
        }
518
        return result.join(path.sep);
249✔
519
    }
520

521
    /**
522
     * Compute the relative path from the source file to the target file
523
     * @param pkgSrcPath  - the absolute path to the source, where cwd is the package location
524
     * @param pkgTargetPath  - the absolute path to the target, where cwd is the package location
525
     */
526
    public getRelativePath(pkgSrcPath: string, pkgTargetPath: string) {
527
        pkgSrcPath = path.normalize(pkgSrcPath);
11✔
528
        pkgTargetPath = path.normalize(pkgTargetPath);
11✔
529

530
        //break by path separator
531
        let sourceParts = pkgSrcPath.split(path.sep);
11✔
532
        let targetParts = pkgTargetPath.split(path.sep);
11✔
533

534
        let commonParts = [] as string[];
11✔
535
        //find their common root
536
        for (let i = 0; i < targetParts.length; i++) {
11✔
537
            if (targetParts[i].toLowerCase() === sourceParts[i].toLowerCase()) {
17✔
538
                commonParts.push(targetParts[i]);
6✔
539
            } else {
540
                //we found a non-matching part...so no more commonalities past this point
541
                break;
11✔
542
            }
543
        }
544

545
        //throw out the common parts from both sets
546
        sourceParts.splice(0, commonParts.length);
11✔
547
        targetParts.splice(0, commonParts.length);
11✔
548

549
        //throw out the filename part of source
550
        sourceParts.splice(sourceParts.length - 1, 1);
11✔
551
        //start out by adding updir paths for each remaining source part
552
        let resultParts = sourceParts.map(() => '..');
11✔
553

554
        //now add every target part
555
        resultParts = [...resultParts, ...targetParts];
11✔
556
        return path.join(...resultParts);
11✔
557
    }
558

559
    public getImportPackagePath(srcPath: string, pkgTargetPath: string) {
560
        const srcExt = this.getExtension(srcPath);
6✔
561
        const lowerSrcExt = srcExt.toLowerCase();
6✔
562
        const lowerTargetExt = this.getExtension(pkgTargetPath).toLowerCase();
6✔
563
        if (lowerSrcExt === '.bs' && lowerTargetExt === '.brs') {
6✔
564
            // if source is .bs, use that as the import extenstion
565
            return pkgTargetPath.substring(0, pkgTargetPath.length - lowerTargetExt.length) + srcExt;
3✔
566
        }
567
        return pkgTargetPath;
3✔
568
    }
569

570
    /**
571
     * Walks left in a DottedGetExpression and returns a VariableExpression if found, or undefined if not found
572
     */
573
    public findBeginningVariableExpression(dottedGet: DottedGetExpression): VariableExpression | undefined {
574
        let left: any = dottedGet;
50✔
575
        while (left) {
50✔
576
            if (isVariableExpression(left)) {
82✔
577
                return left;
50✔
578
            } else if (isDottedGetExpression(left)) {
32!
579
                left = left.obj;
32✔
580
            } else {
581
                break;
×
582
            }
583
        }
584
    }
585

586
    /**
587
     * Do `a` and `b` overlap by at least one character. This returns false if they are at the edges. Here's some examples:
588
     * ```
589
     * | true | true | true | true | true | false | false | false | false |
590
     * |------|------|------|------|------|-------|-------|-------|-------|
591
     * | aa   |  aaa |  aaa | aaa  |  a   |  aa   |    aa | a     |     a |
592
     * |  bbb | bb   |  bbb |  b   | bbb  |    bb |  bb   |     b | a     |
593
     * ```
594
     */
595
    public rangesIntersect(a: Range | undefined, b: Range | undefined) {
596
        //stop if the either range is misisng
597
        if (!a || !b) {
11✔
598
            return false;
2✔
599
        }
600

601
        // Check if `a` is before `b`
602
        if (a.end.line < b.start.line || (a.end.line === b.start.line && a.end.character <= b.start.character)) {
9✔
603
            return false;
1✔
604
        }
605

606
        // Check if `b` is before `a`
607
        if (b.end.line < a.start.line || (b.end.line === a.start.line && b.end.character <= a.start.character)) {
8✔
608
            return false;
1✔
609
        }
610

611
        // These ranges must intersect
612
        return true;
7✔
613
    }
614

615
    /**
616
     * Do `a` and `b` overlap by at least one character or touch at the edges
617
     * ```
618
     * | true | true | true | true | true | true  | true  | false | false |
619
     * |------|------|------|------|------|-------|-------|-------|-------|
620
     * | aa   |  aaa |  aaa | aaa  |  a   |  aa   |    aa | a     |     a |
621
     * |  bbb | bb   |  bbb |  b   | bbb  |    bb |  bb   |     b | a     |
622
     * ```
623
     */
624
    public rangesIntersectOrTouch(a: Range | undefined, b: Range | undefined) {
625
        //stop if the either range is misisng
626
        if (!a || !b) {
34✔
627
            return false;
2✔
628
        }
629
        // Check if `a` is before `b`
630
        if (a.end.line < b.start.line || (a.end.line === b.start.line && a.end.character < b.start.character)) {
32✔
631
            return false;
2✔
632
        }
633

634
        // Check if `b` is before `a`
635
        if (b.end.line < a.start.line || (b.end.line === a.start.line && b.end.character < a.start.character)) {
30✔
636
            return false;
2✔
637
        }
638

639
        // These ranges must intersect
640
        return true;
28✔
641
    }
642

643
    /**
644
     * Test if `position` is in `range`. If the position is at the edges, will return true.
645
     * Adapted from core vscode
646
     */
647
    public rangeContains(range: Range | undefined, position: Position | undefined) {
648
        return this.comparePositionToRange(position, range) === 0;
12,350✔
649
    }
650

651
    public comparePositionToRange(position: Position | undefined, range: Range | undefined) {
652
        //stop if the either range is missng
653
        if (!position || !range) {
14,497✔
654
            return 0;
3✔
655
        }
656

657
        if (this.comparePosition(position, range.start) < 0) {
14,494✔
658
            return -1;
2,249✔
659
        }
660
        if (this.comparePosition(position, range.end) > 0) {
12,245✔
661
            return 1;
8,798✔
662
        }
663
        return 0;
3,447✔
664
    }
665

666
    public comparePosition(a: Position | undefined, b: Position) {
667
        //stop if the either position is missing
668
        if (!a || !b) {
235,152!
669
            return 0;
×
670
        }
671

672
        if (a.line < b.line || (a.line === b.line && a.character < b.character)) {
235,152✔
673
            return -1;
16,578✔
674
        }
675
        if (a.line > b.line || (a.line === b.line && a.character > b.character)) {
218,574✔
676
            return 1;
217,528✔
677
        }
678
        return 0;
1,046✔
679
    }
680

681
    /**
682
     * Is the inner range completely enclosed in the outer range
683
     */
684
    public isRangeInRange(inner: Range, outer: Range) {
685
        return this.comparePosition(inner.start, outer.start) === 1 &&
5✔
686
            this.comparePosition(inner.end, outer.end) === -1;
687
    }
688

689
    /**
690
     * Combine all the documentation for a node - uses the AstNode's leadingTrivia property
691
     * @param node the node to get the documentation for
692
     * @param options extra options
693
     * @param options.prettyPrint if true, will format the comment text for markdown
694
     * @param options.commentTokens out Array of tokens that match the comment lines
695
     */
696
    public getNodeDocumentation(node: AstNode, options: { prettyPrint?: boolean; commentTokens?: Token[] } = { prettyPrint: true }) {
1,046✔
697
        if (!node) {
34,765✔
698
            return '';
1,346✔
699
        }
700
        options = options ?? { prettyPrint: true };
33,419!
701
        options.commentTokens = options.commentTokens ?? [];
33,419✔
702
        const nodeTrivia = node.leadingTrivia ?? [];
33,419✔
703
        const leadingTrivia = isStatement(node)
33,419✔
704
            ? [...(node.annotations?.map(anno => anno.leadingTrivia ?? []).flat() ?? []), ...nodeTrivia]
32!
705
            : nodeTrivia;
706
        const tokens = leadingTrivia?.filter(t => t.kind === TokenKind.Newline || t.kind === TokenKind.Comment);
64,592!
707
        const comments = [] as Token[];
33,419✔
708

709
        let newLinesInRow = 0;
33,419✔
710
        for (let i = tokens.length - 1; i >= 0; i--) {
33,419✔
711
            const token = tokens[i];
29,692✔
712
            //skip whitespace and newline chars
713
            if (token.kind === TokenKind.Comment) {
29,692✔
714
                comments.push(token);
1,081✔
715
                newLinesInRow = 0;
1,081✔
716
            } else if (token.kind === TokenKind.Newline) {
28,611!
717
                //skip these tokens
718
                newLinesInRow++;
28,611✔
719

720
                if (newLinesInRow > 1) {
28,611✔
721
                    // stop processing on empty line.
722
                    break;
3,360✔
723
                }
724
                //any other token means there are no more comments
725
            } else {
726
                break;
×
727
            }
728
        }
729
        const jsDocCommentBlockLine = /(\/\*{2,}|\*{1,}\/)/i;
33,419✔
730
        let usesjsDocCommentBlock = false;
33,419✔
731
        if (comments.length === 0) {
33,419✔
732
            return '';
32,493✔
733
        }
734
        return comments.reverse()
926✔
735
            .map(x => ({ line: x.text.replace(/^('|rem)/i, '').trim(), token: x }))
1,081✔
736
            .filter(({ line }) => {
737
                if (jsDocCommentBlockLine.exec(line)) {
1,081✔
738
                    usesjsDocCommentBlock = true;
46✔
739
                    return false;
46✔
740
                }
741
                return true;
1,035✔
742
            }).map(({ line, token }) => {
743
                if (usesjsDocCommentBlock) {
1,035✔
744
                    if (line.startsWith('*')) {
39✔
745
                        //remove jsDoc leading '*'
746
                        line = line.slice(1).trim();
38✔
747
                    }
748
                }
749
                if (options.prettyPrint && line.startsWith('@')) {
1,035✔
750
                    // Handle jsdoc/brightscriptdoc tags specially
751
                    // make sure they are on their own markdown line, and add italics
752
                    const firstSpaceIndex = line.indexOf(' ');
3✔
753
                    if (firstSpaceIndex === -1) {
3✔
754
                        return `\n_${line}_`;
1✔
755
                    }
756
                    const firstWord = line.substring(0, firstSpaceIndex);
2✔
757
                    return `\n_${firstWord}_ ${line.substring(firstSpaceIndex + 1)}`;
2✔
758
                }
759
                if (options.commentTokens) {
1,032!
760
                    options.commentTokens.push(token);
1,032✔
761
                }
762
                return line;
1,032✔
763
            }).join('\n');
764
    }
765

766
    /**
767
     * Prefixes a component name so it can be used as type in the symbol table, without polluting available symbols
768
     *
769
     * @param sgNodeName the Name of the component
770
     * @returns the node name, prefixed with `roSGNode`
771
     */
772
    public getSgNodeTypeName(sgNodeName: string) {
773
        return 'roSGNode' + sgNodeName;
373,188✔
774
    }
775

776
    /**
777
     * Parse an xml file and get back a javascript object containing its results
778
     */
779
    public parseXml(text: string) {
780
        return new Promise<any>((resolve, reject) => {
×
781
            xml2js.parseString(text, (err, data) => {
×
782
                if (err) {
×
783
                    reject(err);
×
784
                } else {
785
                    resolve(data);
×
786
                }
787
            });
788
        });
789
    }
790

791
    public propertyCount(object: Record<string, unknown>) {
792
        let count = 0;
×
793
        for (let key in object) {
×
794
            if (object.hasOwnProperty(key)) {
×
795
                count++;
×
796
            }
797
        }
798
        return count;
×
799
    }
800

801
    public padLeft(subject: string, totalLength: number, char: string) {
802
        totalLength = totalLength > 1000 ? 1000 : totalLength;
1!
803
        while (subject.length < totalLength) {
1✔
804
            subject = char + subject;
1,000✔
805
        }
806
        return subject;
1✔
807
    }
808

809
    /**
810
     * Force the drive letter to lower case
811
     */
812
    public driveLetterToLower(fullPath: string) {
813
        if (fullPath) {
2!
814
            let firstCharCode = fullPath.charCodeAt(0);
2✔
815
            if (
2!
816
                //is upper case A-Z
817
                firstCharCode >= 65 && firstCharCode <= 90 &&
6✔
818
                //next char is colon
819
                fullPath[1] === ':'
820
            ) {
821
                fullPath = fullPath[0].toLowerCase() + fullPath.substring(1);
2✔
822
            }
823
        }
824
        return fullPath;
2✔
825
    }
826

827
    /**
828
     * Replace the first instance of `search` in `subject` with `replacement`
829
     */
830
    public replaceCaseInsensitive(subject: string, search: string, replacement: string) {
831
        let idx = subject.toLowerCase().indexOf(search.toLowerCase());
7,712✔
832
        if (idx > -1) {
7,712✔
833
            let result = subject.substring(0, idx) + replacement + subject.substring(idx + search.length);
2,397✔
834
            return result;
2,397✔
835
        } else {
836
            return subject;
5,315✔
837
        }
838
    }
839

840
    /**
841
     * Determine if two arrays containing primitive values are equal.
842
     * This considers order and compares by equality.
843
     */
844
    public areArraysEqual(arr1: any[], arr2: any[]) {
845
        if (arr1.length !== arr2.length) {
8✔
846
            return false;
3✔
847
        }
848
        for (let i = 0; i < arr1.length; i++) {
5✔
849
            if (arr1[i] !== arr2[i]) {
7✔
850
                return false;
3✔
851
            }
852
        }
853
        return true;
2✔
854
    }
855
    /**
856
     * Does the string appear to be a uri (i.e. does it start with `file:`)
857
     */
858
    public isUriLike(filePath: string) {
859
        return filePath?.indexOf('file:') === 0;// eslint-disable-line @typescript-eslint/prefer-string-starts-ends-with
307,457✔
860
    }
861

862
    /**
863
     * Given a file path, convert it to a URI string
864
     */
865
    public pathToUri(filePath: string) {
866
        if (!filePath) {
277,473✔
867
            return filePath;
32,295✔
868
        } else if (this.isUriLike(filePath)) {
245,178✔
869
            return filePath;
220,895✔
870
        } else {
871
            return URI.file(filePath).toString();
24,283✔
872
        }
873
    }
874

875
    /**
876
     * Given a URI, convert that to a regular fs path
877
     */
878
    public uriToPath(uri: string) {
879
        //if this doesn't look like a URI, then assume it's already a path
880
        if (this.isUriLike(uri) === false) {
58,319✔
881
            return uri;
26✔
882
        }
883
        let parsedPath = URI.parse(uri).fsPath;
58,293✔
884

885
        //Uri annoyingly converts all drive letters to lower case...so this will bring back whatever case it came in as
886
        let match = /\/\/\/([a-z]:)/i.exec(uri);
58,293✔
887
        if (match) {
58,293✔
888
            let originalDriveCasing = match[1];
149✔
889
            parsedPath = originalDriveCasing + parsedPath.substring(2);
149✔
890
        }
891
        const normalizedPath = path.normalize(parsedPath);
58,293✔
892
        return normalizedPath;
58,293✔
893
    }
894

895

896
    /**
897
     * Get the outDir from options, taking into account cwd and absolute outFile paths
898
     */
899
    public getOutDir(options: FinalizedBsConfig) {
900
        options = this.normalizeConfig(options);
2✔
901
        let cwd = path.normalize(options.cwd ? options.cwd : process.cwd());
2!
902
        if (path.isAbsolute(options.outFile)) {
2!
903
            return path.dirname(options.outFile);
×
904
        } else {
905
            return path.normalize(path.join(cwd, path.dirname(options.outFile)));
2✔
906
        }
907
    }
908

909
    /**
910
     * Get paths to all files on disc that match this project's source list
911
     */
912
    public async getFilePaths(options: FinalizedBsConfig) {
913
        let rootDir = this.getRootDir(options);
97✔
914

915
        let files = await rokuDeploy.getFilePaths(options.files, rootDir);
97✔
916
        return files;
97✔
917
    }
918

919
    /**
920
     * Given a path to a brs file, compute the path to a theoretical d.bs file.
921
     * Only `.brs` files can have typedef path, so return undefined for everything else
922
     */
923
    public getTypedefPath(brsSrcPath: string) {
924
        const typedefPath = brsSrcPath
3,890✔
925
            .replace(/\.brs$/i, '.d.bs')
926
            .toLowerCase();
927

928
        if (typedefPath.endsWith('.d.bs')) {
3,890✔
929
            return typedefPath;
2,262✔
930
        } else {
931
            return undefined;
1,628✔
932
        }
933
    }
934

935

936
    /**
937
     * Walks up the chain to find the closest bsconfig.json file
938
     */
939
    public async findClosestConfigFile(currentPath: string): Promise<string | undefined> {
940
        //make the path absolute
941
        currentPath = path.resolve(
4✔
942
            path.normalize(
943
                currentPath
944
            )
945
        );
946

947
        let previousPath: string | undefined;
948
        //using ../ on the root of the drive results in the same file path, so that's how we know we reached the top
949
        while (previousPath !== currentPath) {
4✔
950
            previousPath = currentPath;
10✔
951

952
            let bsPath = path.join(currentPath, 'bsconfig.json');
10✔
953
            let brsPath = path.join(currentPath, 'brsconfig.json');
10✔
954
            if (await this.pathExists(bsPath)) {
10✔
955
                return bsPath;
2✔
956
            } else if (await this.pathExists(brsPath)) {
8✔
957
                return brsPath;
2✔
958
            } else {
959
                //walk upwards one directory
960
                currentPath = path.resolve(path.join(currentPath, '../'));
6✔
961
            }
962
        }
963
        //got to the root path, no config file exists
964
    }
965

966
    /**
967
     * Set a timeout for the specified milliseconds, and resolve the promise once the timeout is finished.
968
     * @param milliseconds the minimum number of milliseconds to sleep for
969
     */
970
    public sleep(milliseconds: number) {
971
        return new Promise((resolve) => {
955✔
972
            //if milliseconds is 0, don't actually timeout (improves unit test throughput)
973
            if (milliseconds === 0) {
955✔
974
                process.nextTick(resolve);
817✔
975
            } else {
976
                setTimeout(resolve, milliseconds);
138✔
977
            }
978
        });
979
    }
980

981
    /**
982
     * Given an array, map and then flatten
983
     * @param array the array to flatMap over
984
     * @param callback a function that is called for every array item
985
     */
986
    public flatMap<T, R>(array: T[], callback: (arg: T) => R[]): R[] {
987
        return Array.prototype.concat.apply([], array.map(callback));
×
988
    }
989

990
    /**
991
     * Determines if the position is greater than the range. This means
992
     * the position does not touch the range, and has a position greater than the end
993
     * of the range. A position that touches the last line/char of a range is considered greater
994
     * than the range, because the `range.end` is EXclusive
995
     */
996
    public positionIsGreaterThanRange(position: Position, range: Range) {
997

998
        //if the position is a higher line than the range
999
        if (position.line > range.end.line) {
1,291✔
1000
            return true;
1,203✔
1001
        } else if (position.line < range.end.line) {
88!
1002
            return false;
×
1003
        }
1004
        //they are on the same line
1005

1006
        //if the position's char is greater than or equal to the range's
1007
        if (position.character >= range.end.character) {
88!
1008
            return true;
88✔
1009
        } else {
1010
            return false;
×
1011
        }
1012
    }
1013

1014
    /**
1015
     * Get a range back from an object that contains (or is) a range
1016
     */
1017
    public extractRange(rangeIsh: RangeLike): Range | undefined {
1018
        if (!rangeIsh) {
10,536✔
1019
            return undefined;
29✔
1020
        } else if ('location' in rangeIsh) {
10,507✔
1021
            return rangeIsh.location?.range;
7,292✔
1022
        } else if ('range' in rangeIsh) {
3,215!
1023
            return rangeIsh.range;
3,215✔
1024
        } else if (Range.is(rangeIsh)) {
×
1025
            return rangeIsh;
×
1026
        } else {
1027
            return undefined;
×
1028
        }
1029
    }
1030

1031

1032
    /**
1033
     * Get a location object back by extracting location information from other objects that contain location
1034
     */
1035
    public getRange(startObj: | { range: Range }, endObj: { range: Range }): Range {
1036
        if (!startObj?.range || !endObj?.range) {
×
1037
            return undefined;
×
1038
        }
1039
        return util.createRangeFromPositions(startObj.range?.start, endObj.range?.end);
×
1040
    }
1041

1042
    /**
1043
     * If the two items both start on the same line
1044
     */
1045
    public sameStartLine(first: { range: Range }, second: { range: Range }) {
1046
        if (first && second && first.range.start.line === second.range.start.line) {
×
1047
            return true;
×
1048
        } else {
1049
            return false;
×
1050
        }
1051
    }
1052

1053
    /**
1054
     * If the two items have lines that touch
1055
     */
1056
    public linesTouch(first: RangeLike, second: RangeLike) {
1057
        const firstRange = this.extractRange(first);
1,636✔
1058
        const secondRange = this.extractRange(second);
1,636✔
1059
        if (firstRange && secondRange && (
1,636✔
1060
            firstRange.start.line === secondRange.start.line ||
1061
            firstRange.start.line === secondRange.end.line ||
1062
            firstRange.end.line === secondRange.start.line ||
1063
            firstRange.end.line === secondRange.end.line
1064
        )) {
1065
            return true;
91✔
1066
        } else {
1067
            return false;
1,545✔
1068
        }
1069
    }
1070

1071
    /**
1072
     * Given text with (or without) dots separating text, get the rightmost word.
1073
     * (i.e. given "A.B.C", returns "C". or "B" returns "B because there's no dot)
1074
     */
1075
    public getTextAfterFinalDot(name: string) {
1076
        if (name) {
×
1077
            let parts = name.split('.');
×
1078
            if (parts.length > 0) {
×
1079
                return parts[parts.length - 1];
×
1080
            }
1081
        }
1082
    }
1083

1084
    /**
1085
     * Find a script import that the current position touches, or undefined if not found
1086
     */
1087
    public getScriptImportAtPosition(scriptImports: FileReference[], position: Position): FileReference | undefined {
1088
        let scriptImport = scriptImports.find((x) => {
127✔
1089
            return x.filePathRange &&
5✔
1090
                x.filePathRange.start.line === position.line &&
1091
                //column between start and end
1092
                position.character >= x.filePathRange.start.character &&
1093
                position.character <= x.filePathRange.end.character;
1094
        });
1095
        return scriptImport;
127✔
1096
    }
1097

1098
    /**
1099
     * Given the class name text, return a namespace-prefixed name.
1100
     * If the name already has a period in it, or the namespaceName was not provided, return the class name as is.
1101
     * If the name does not have a period, and a namespaceName was provided, return the class name prepended by the namespace name.
1102
     * If no namespace is provided, return the `className` unchanged.
1103
     */
1104
    public getFullyQualifiedClassName(className: string, namespaceName?: string) {
1105
        if (className?.includes('.') === false && namespaceName) {
4,275✔
1106
            return `${namespaceName}.${className}`;
224✔
1107
        } else {
1108
            return className;
4,051✔
1109
        }
1110
    }
1111

1112
    public splitIntoLines(string: string) {
1113
        return string.split(/\r?\n/g);
169✔
1114
    }
1115

1116
    public getTextForRange(string: string | string[], range: Range): string {
1117
        let lines: string[];
1118
        if (Array.isArray(string)) {
171✔
1119
            lines = string;
170✔
1120
        } else {
1121
            lines = this.splitIntoLines(string);
1✔
1122
        }
1123

1124
        const start = range.start;
171✔
1125
        const end = range.end;
171✔
1126

1127
        let endCharacter = end.character;
171✔
1128
        // If lines are the same we need to subtract out our new starting position to make it work correctly
1129
        if (start.line === end.line) {
171✔
1130
            endCharacter -= start.character;
159✔
1131
        }
1132

1133
        let rangeLines = [lines[start.line].substring(start.character)];
171✔
1134
        for (let i = start.line + 1; i <= end.line; i++) {
171✔
1135
            rangeLines.push(lines[i]);
12✔
1136
        }
1137
        const lastLine = rangeLines.pop();
171✔
1138
        if (lastLine !== undefined) {
171!
1139
            rangeLines.push(lastLine.substring(0, endCharacter));
171✔
1140
        }
1141
        return rangeLines.join('\n');
171✔
1142
    }
1143

1144
    /**
1145
     * Helper for creating `Location` objects. Prefer using this function because vscode-languageserver's `Location.create()` is significantly slower at scale
1146
     */
1147
    public createLocationFromRange(uri: string, range: Range): Location {
1148
        return {
9,281✔
1149
            uri: util.pathToUri(uri),
1150
            range: range
1151
        };
1152
    }
1153

1154
    /**
1155
     * Helper for creating `Location` objects from a file and range
1156
     */
1157
    public createLocationFromFileRange(file: BscFile, range: Range): Location {
1158
        return this.createLocationFromRange(this.pathToUri(file?.srcPath), range);
406!
1159
    }
1160

1161
    /**
1162
     * Helper for creating `Location` objects by passing each range value in directly. Prefer using this function because vscode-languageserver's `Location.create()` is significantly slower at scale
1163
     */
1164
    public createLocation(startLine: number, startCharacter: number, endLine: number, endCharacter: number, uri?: string): Location {
1165
        return {
256,037✔
1166
            uri: util.pathToUri(uri),
1167
            range: {
1168
                start: {
1169
                    line: startLine,
1170
                    character: startCharacter
1171
                },
1172
                end: {
1173
                    line: endLine,
1174
                    character: endCharacter
1175
                }
1176
            }
1177
        };
1178
    }
1179

1180
    /**
1181
     * Helper for creating `Range` objects. Prefer using this function because vscode-languageserver's `Range.create()` is significantly slower.
1182
     */
1183
    public createRange(startLine: number, startCharacter: number, endLine: number, endCharacter: number): Range {
1184
        return {
9,376✔
1185
            start: {
1186
                line: startLine,
1187
                character: startCharacter
1188
            },
1189
            end: {
1190
                line: endLine,
1191
                character: endCharacter
1192
            }
1193
        };
1194
    }
1195

1196
    /**
1197
     * Create a `Range` from two `Position`s
1198
     */
1199
    public createRangeFromPositions(startPosition: Position, endPosition: Position): Range | undefined {
1200
        startPosition = startPosition ?? endPosition;
171✔
1201
        endPosition = endPosition ?? startPosition;
171✔
1202
        if (!startPosition && !endPosition) {
171!
1203
            return undefined;
×
1204
        }
1205
        return this.createRange(startPosition.line, startPosition.character, endPosition.line, endPosition.character);
171✔
1206
    }
1207

1208
    /**
1209
     * Clone a range
1210
     */
1211
    public cloneLocation(location: Location) {
1212
        if (location) {
16,208✔
1213
            return {
15,996✔
1214
                uri: location.uri,
1215
                range: {
1216
                    start: {
1217
                        line: location.range.start.line,
1218
                        character: location.range.start.character
1219
                    },
1220
                    end: {
1221
                        line: location.range.end.line,
1222
                        character: location.range.end.character
1223
                    }
1224
                }
1225
            };
1226
        } else {
1227
            return location;
212✔
1228
        }
1229
    }
1230

1231
    /**
1232
     * Clone every token
1233
     */
1234
    public cloneToken<T extends Token>(token: T): T {
1235
        if (token) {
2,875✔
1236
            const result = {
2,668✔
1237
                kind: token.kind,
1238
                location: this.cloneLocation(token.location),
1239
                text: token.text,
1240
                isReserved: token.isReserved,
1241
                leadingWhitespace: token.leadingWhitespace,
1242
                leadingTrivia: token.leadingTrivia.map(x => this.cloneToken(x))
1,357✔
1243
            } as Token;
1244
            //handle those tokens that have charCode
1245
            if ('charCode' in token) {
2,668✔
1246
                (result as any).charCode = (token as any).charCode;
3✔
1247
            }
1248
            return result as T;
2,668✔
1249
        } else {
1250
            return token;
207✔
1251
        }
1252
    }
1253

1254
    /**
1255
     *  Gets the bounding range of a bunch of ranges or objects that have ranges
1256
     *  TODO: this does a full iteration of the args. If the args were guaranteed to be in range order, we could optimize this
1257
     */
1258
    public createBoundingLocation(...locatables: Array<{ location?: Location } | Location | { range?: Range } | Range | undefined>): Location | undefined {
1259
        let uri: string | undefined;
1260
        let startPosition: Position | undefined;
1261
        let endPosition: Position | undefined;
1262

1263
        for (let locatable of locatables) {
49,795✔
1264
            let range: Range;
1265
            if (!locatable) {
202,559✔
1266
                continue;
54,481✔
1267
            } else if ('location' in locatable) {
148,078✔
1268
                range = locatable.location?.range;
142,302✔
1269
                if (!uri) {
142,302✔
1270
                    uri = locatable.location?.uri;
56,491✔
1271
                }
1272
            } else if (Location.is(locatable)) {
5,776✔
1273
                range = locatable.range;
5,764✔
1274
                if (!uri) {
5,764✔
1275
                    uri = locatable.uri;
5,138✔
1276
                }
1277
            } else if ('range' in locatable) {
12!
1278
                range = locatable.range;
×
1279
            } else {
1280
                range = locatable as Range;
12✔
1281
            }
1282

1283
            //skip undefined locations or locations without a range
1284
            if (!range) {
148,078✔
1285
                continue;
3,680✔
1286
            }
1287

1288
            if (!startPosition) {
144,398✔
1289
                startPosition = range.start;
48,562✔
1290
            } else if (this.comparePosition(range.start, startPosition) < 0) {
95,836✔
1291
                startPosition = range.start;
941✔
1292
            }
1293
            if (!endPosition) {
144,398✔
1294
                endPosition = range.end;
48,562✔
1295
            } else if (this.comparePosition(range.end, endPosition) > 0) {
95,836✔
1296
                endPosition = range.end;
90,032✔
1297
            }
1298
        }
1299
        if (startPosition && endPosition) {
49,795✔
1300
            return util.createLocation(startPosition.line, startPosition.character, endPosition.line, endPosition.character, uri);
48,562✔
1301
        } else {
1302
            return undefined;
1,233✔
1303
        }
1304
    }
1305

1306
    /**
1307
     *  Gets the bounding range of a bunch of ranges or objects that have ranges
1308
     *  TODO: this does a full iteration of the args. If the args were guaranteed to be in range order, we could optimize this
1309
     */
1310
    public createBoundingRange(...locatables: Array<RangeLike>): Range | undefined {
1311
        return this.createBoundingLocation(...locatables)?.range;
544✔
1312
    }
1313

1314
    /**
1315
     * Gets the bounding range of an object that contains a bunch of tokens
1316
     * @param tokens Object with tokens in it
1317
     * @returns Range containing all the tokens
1318
     */
1319
    public createBoundingLocationFromTokens(tokens: Record<string, { location?: Location }>): Location | undefined {
1320
        let uri: string;
1321
        let startPosition: Position | undefined;
1322
        let endPosition: Position | undefined;
1323
        for (let key in tokens) {
5,572✔
1324
            let token = tokens?.[key];
19,806!
1325
            let locatableRange = token?.location?.range;
19,806✔
1326
            if (!locatableRange) {
19,806✔
1327
                continue;
6,031✔
1328
            }
1329

1330
            if (!startPosition) {
13,775✔
1331
                startPosition = locatableRange.start;
5,413✔
1332
            } else if (this.comparePosition(locatableRange.start, startPosition) < 0) {
8,362✔
1333
                startPosition = locatableRange.start;
2,291✔
1334
            }
1335
            if (!endPosition) {
13,775✔
1336
                endPosition = locatableRange.end;
5,413✔
1337
            } else if (this.comparePosition(locatableRange.end, endPosition) > 0) {
8,362✔
1338
                endPosition = locatableRange.end;
5,869✔
1339
            }
1340
            if (!uri) {
13,775✔
1341
                uri = token.location.uri;
6,583✔
1342
            }
1343
        }
1344
        if (startPosition && endPosition) {
5,572✔
1345
            return this.createLocation(startPosition.line, startPosition.character, endPosition.line, endPosition.character, uri);
5,413✔
1346
        } else {
1347
            return undefined;
159✔
1348
        }
1349
    }
1350

1351
    /**
1352
     * Create a `Position` object. Prefer this over `Position.create` for performance reasons.
1353
     */
1354
    public createPosition(line: number, character: number) {
1355
        return {
386✔
1356
            line: line,
1357
            character: character
1358
        };
1359
    }
1360

1361
    /**
1362
     * Convert a list of tokens into a string, including their leading whitespace
1363
     */
1364
    public tokensToString(tokens: Token[]) {
1365
        let result = '';
1✔
1366
        //skip iterating the final token
1367
        for (let token of tokens) {
1✔
1368
            result += token.leadingWhitespace + token.text;
16✔
1369
        }
1370
        return result;
1✔
1371
    }
1372

1373
    /**
1374
     * Convert a token into a BscType
1375
     */
1376
    public tokenToBscType(token: Token) {
1377
        // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
1378
        switch (token.kind) {
2,114,470✔
1379
            case TokenKind.Boolean:
2,118,131✔
1380
                return BooleanType.instance;
240✔
1381
            case TokenKind.True:
1382
            case TokenKind.False:
1383
                return BooleanType.instance;
205✔
1384
            case TokenKind.Double:
1385
                return DoubleType.instance;
93✔
1386
            case TokenKind.DoubleLiteral:
1387
                return DoubleType.instance;
9✔
1388
            case TokenKind.Dynamic:
1389
                return DynamicType.instance;
153✔
1390
            case TokenKind.Float:
1391
                return FloatType.instance;
443✔
1392
            case TokenKind.FloatLiteral:
1393
                return FloatType.instance;
123✔
1394
            case TokenKind.Function:
1395
                return FunctionType.instance;
158✔
1396
            case TokenKind.Integer:
1397
                return IntegerType.instance;
1,483✔
1398
            case TokenKind.IntegerLiteral:
1399
                return IntegerType.instance;
1,981✔
1400
            case TokenKind.Invalid:
1401
                return InvalidType.instance;
110✔
1402
            case TokenKind.LongInteger:
1403
                return LongIntegerType.instance;
86✔
1404
            case TokenKind.LongIntegerLiteral:
1405
                return LongIntegerType.instance;
10✔
1406
            case TokenKind.Object:
1407
                return ObjectType.instance;
430✔
1408
            case TokenKind.String:
1409
                return StringType.instance;
2,653✔
1410
            case TokenKind.StringLiteral:
1411
            case TokenKind.TemplateStringExpressionBegin:
1412
            case TokenKind.TemplateStringExpressionEnd:
1413
            case TokenKind.TemplateStringQuasi:
1414
                return StringType.instance;
1,172✔
1415
            case TokenKind.Void:
1416
                return VoidType.instance;
130✔
1417
            case TokenKind.Identifier:
1418
                switch (token.text.toLowerCase()) {
2,104,968✔
1419
                    case 'boolean':
1,147,650!
1420
                        return BooleanType.instance;
267,580✔
1421
                    case 'double':
1422
                        return DoubleType.instance;
5✔
1423
                    case 'dynamic':
1424
                        return DynamicType.instance;
5✔
1425
                    case 'float':
1426
                        return FloatType.instance;
251,723✔
1427
                    case 'function':
1428
                        return FunctionType.instance;
×
1429
                    case 'integer':
1430
                        return IntegerType.instance;
212,084✔
1431
                    case 'invalid':
1432
                        return InvalidType.instance;
×
1433
                    case 'longinteger':
1434
                        return LongIntegerType.instance;
5✔
1435
                    case 'object':
1436
                        return ObjectType.instance;
5✔
1437
                    case 'string':
1438
                        return StringType.instance;
416,238✔
1439
                    case 'void':
1440
                        return VoidType.instance;
5✔
1441
                }
1442
        }
1443
    }
1444

1445
    /**
1446
     * Deciphers the correct types for fields based on docs
1447
     * https://developer.roku.com/en-ca/docs/references/scenegraph/xml-elements/interface.md
1448
     * @param typeDescriptor the type descriptor from the docs
1449
     * @returns {BscType} the known type, or dynamic
1450
     */
1451
    public getNodeFieldType(typeDescriptor: string, lookupTable?: SymbolTable): BscType {
1452
        let typeDescriptorLower = typeDescriptor.toLowerCase().trim().replace(/\*/g, '');
2,085,118✔
1453

1454
        if (typeDescriptorLower.startsWith('as ')) {
2,085,118✔
1455
            typeDescriptorLower = typeDescriptorLower.substring(3).trim();
7,928✔
1456
        }
1457
        const nodeFilter = (new RegExp(/^\[?(.* node)/, 'i')).exec(typeDescriptorLower);
2,085,118✔
1458
        if (nodeFilter?.[1]) {
2,085,118✔
1459
            typeDescriptorLower = nodeFilter[1].trim();
43,604✔
1460
        }
1461
        const parensFilter = (new RegExp(/(.*)\(.*\)/, 'gi')).exec(typeDescriptorLower);
2,085,118✔
1462
        if (parensFilter?.[1]) {
2,085,118✔
1463
            typeDescriptorLower = parensFilter[1].trim();
3,964✔
1464
        }
1465

1466
        const bscType = this.tokenToBscType(createToken(TokenKind.Identifier, typeDescriptorLower));
2,085,118✔
1467
        if (bscType) {
2,085,118✔
1468
            return bscType;
1,147,605✔
1469
        }
1470

1471
        function getRect2dType() {
1472
            const rect2dType = new AssociativeArrayType();
5,950✔
1473
            rect2dType.addMember('height', {}, FloatType.instance, SymbolTypeFlag.runtime);
5,950✔
1474
            rect2dType.addMember('width', {}, FloatType.instance, SymbolTypeFlag.runtime);
5,950✔
1475
            rect2dType.addMember('x', {}, FloatType.instance, SymbolTypeFlag.runtime);
5,950✔
1476
            rect2dType.addMember('y', {}, FloatType.instance, SymbolTypeFlag.runtime);
5,950✔
1477
            return rect2dType;
5,950✔
1478
        }
1479

1480
        function getColorType() {
1481
            return unionTypeFactory([IntegerType.instance, StringType.instance]);
130,816✔
1482
        }
1483

1484
        //check for uniontypes
1485
        const multipleTypes = typeDescriptorLower.split(' or ').map(s => s.trim());
941,477✔
1486
        if (multipleTypes.length > 1) {
937,513✔
1487
            const individualTypes = multipleTypes.map(t => this.getNodeFieldType(t, lookupTable));
7,928✔
1488
            return unionTypeFactory(individualTypes);
3,964✔
1489
        }
1490

1491
        const typeIsArray = typeDescriptorLower.startsWith('array of ') || typeDescriptorLower.startsWith('roarray of ');
933,549✔
1492

1493
        if (typeIsArray) {
933,549✔
1494
            const ofSearch = ' of ';
114,956✔
1495
            const arrayPrefixLength = typeDescriptorLower.indexOf(ofSearch) + ofSearch.length;
114,956✔
1496
            let arrayOfTypeName = typeDescriptorLower.substring(arrayPrefixLength); //cut off beginnin, eg. 'array of' or 'roarray of'
114,956✔
1497
            if (arrayOfTypeName.endsWith('s')) {
114,956✔
1498
                // remove "s" in "floats", etc.
1499
                arrayOfTypeName = arrayOfTypeName.substring(0, arrayOfTypeName.length - 1);
83,244✔
1500
            }
1501
            if (arrayOfTypeName.endsWith('\'')) {
114,956!
1502
                // remove "'" in "float's", etc.
1503
                arrayOfTypeName = arrayOfTypeName.substring(0, arrayOfTypeName.length - 1);
×
1504
            }
1505
            if (arrayOfTypeName === 'rectangle') {
114,956✔
1506
                arrayOfTypeName = 'rect2d';
1,982✔
1507
            }
1508
            let arrayType = this.getNodeFieldType(arrayOfTypeName, lookupTable);
114,956✔
1509
            return new ArrayType(arrayType);
114,956✔
1510
        } else if (typeDescriptorLower.startsWith('option ')) {
818,593✔
1511
            const actualTypeName = typeDescriptorLower.substring('option '.length); //cut off beginning 'option '
39,640✔
1512
            return this.getNodeFieldType(actualTypeName, lookupTable);
39,640✔
1513
        } else if (typeDescriptorLower.startsWith('value ')) {
778,953✔
1514
            const actualTypeName = typeDescriptorLower.substring('value '.length); //cut off beginning 'value '
15,856✔
1515
            return this.getNodeFieldType(actualTypeName, lookupTable);
15,856✔
1516
        } else if (typeDescriptorLower === 'n/a') {
763,097✔
1517
            return DynamicType.instance;
3,964✔
1518
        } else if (typeDescriptorLower === 'uri') {
759,133✔
1519
            return StringType.instance;
142,709✔
1520
        } else if (typeDescriptorLower === 'color') {
616,424✔
1521
            return getColorType();
130,815✔
1522
        } else if (typeDescriptorLower === 'vector2d' || typeDescriptorLower === 'floatarray') {
485,609✔
1523
            return new ArrayType(FloatType.instance);
47,569✔
1524
        } else if (typeDescriptorLower === 'vector2darray') {
438,040!
1525
            return new ArrayType(new ArrayType(FloatType.instance));
×
1526
        } else if (typeDescriptorLower === 'intarray') {
438,040✔
1527
            return new ArrayType(IntegerType.instance);
1✔
1528
        } else if (typeDescriptorLower === 'colorarray') {
438,039✔
1529
            return new ArrayType(getColorType());
1✔
1530
        } else if (typeDescriptorLower === 'boolarray') {
438,038!
1531
            return new ArrayType(BooleanType.instance);
×
1532
        } else if (typeDescriptorLower === 'stringarray' || typeDescriptorLower === 'strarray') {
438,038✔
1533
            return new ArrayType(StringType.instance);
1✔
1534
        } else if (typeDescriptorLower === 'int') {
438,037✔
1535
            return IntegerType.instance;
11,892✔
1536
        } else if (typeDescriptorLower === 'time') {
426,145✔
1537
            return DoubleType.instance;
37,659✔
1538
        } else if (typeDescriptorLower === 'str') {
388,486!
1539
            return StringType.instance;
×
1540
        } else if (typeDescriptorLower === 'bool') {
388,486✔
1541
            return BooleanType.instance;
1,982✔
1542
        } else if (typeDescriptorLower === 'array' || typeDescriptorLower === 'roarray') {
386,504✔
1543
            return new ArrayType();
15,857✔
1544
        } else if (typeDescriptorLower === 'assocarray' ||
370,647✔
1545
            typeDescriptorLower === 'associative array' ||
1546
            typeDescriptorLower === 'associativearray' ||
1547
            typeDescriptorLower === 'roassociativearray' ||
1548
            typeDescriptorLower.startsWith('associative array of') ||
1549
            typeDescriptorLower.startsWith('associativearray of') ||
1550
            typeDescriptorLower.startsWith('roassociativearray of')
1551
        ) {
1552
            return new AssociativeArrayType();
69,371✔
1553
        } else if (typeDescriptorLower === 'node') {
301,276✔
1554
            return ComponentType.instance;
17,839✔
1555
        } else if (typeDescriptorLower === 'nodearray') {
283,437✔
1556
            return new ArrayType(ComponentType.instance);
1✔
1557
        } else if (typeDescriptorLower === 'rect2d') {
283,436✔
1558
            return getRect2dType();
5,948✔
1559
        } else if (typeDescriptorLower === 'rect2darray') {
277,488✔
1560
            return new ArrayType(getRect2dType());
2✔
1561
        } else if (typeDescriptorLower === 'font') {
277,486✔
1562
            return this.getNodeFieldType('roSGNodeFont', lookupTable);
43,606✔
1563
        } else if (typeDescriptorLower === 'contentnode') {
233,880✔
1564
            return this.getNodeFieldType('roSGNodeContentNode', lookupTable);
39,640✔
1565
        } else if (typeDescriptorLower.endsWith(' node')) {
194,240✔
1566
            return this.getNodeFieldType('roSgNode' + typeDescriptorLower.substring(0, typeDescriptorLower.length - 5), lookupTable);
41,622✔
1567
        } else if (lookupTable) {
152,618!
1568
            //try doing a lookup
1569
            return lookupTable.getSymbolType(typeDescriptorLower, {
152,618✔
1570
                flags: SymbolTypeFlag.typetime,
1571
                fullName: typeDescriptor,
1572
                tableProvider: () => lookupTable
56✔
1573
            });
1574
        }
1575

1576
        return DynamicType.instance;
×
1577
    }
1578

1579
    /**
1580
     * Return the type of the result of a binary operator
1581
     * Note: compound assignments (eg. +=) internally use a binary expression, so that's why TokenKind.PlusEqual, etc. are here too
1582
     */
1583
    public binaryOperatorResultType(leftType: BscType, operator: Token, rightType: BscType): BscType {
1584
        if ((isAnyReferenceType(leftType) && !leftType.isResolvable()) ||
696✔
1585
            (isAnyReferenceType(rightType) && !rightType.isResolvable())) {
1586
            return new BinaryOperatorReferenceType(leftType, operator, rightType, (lhs, op, rhs) => {
32✔
1587
                return this.binaryOperatorResultType(lhs, op, rhs);
5✔
1588
            });
1589
        }
1590

1591
        // Try to find a common value of union type
1592
        leftType = getUniqueType([leftType], unionTypeFactory);
664✔
1593
        rightType = getUniqueType([rightType], unionTypeFactory);
664✔
1594

1595
        if (isUnionType(leftType)) {
664✔
1596
            leftType = this.getHighestPriorityType(leftType.types);
22✔
1597
        }
1598
        if (isUnionType(rightType)) {
664✔
1599
            rightType = this.getHighestPriorityType(rightType.types);
2✔
1600
        }
1601

1602
        if (isVoidType(leftType) || isVoidType(rightType) || isUninitializedType(leftType) || isUninitializedType(rightType)) {
664✔
1603
            return undefined;
1✔
1604
        }
1605

1606
        if (isEnumMemberType(leftType)) {
663✔
1607
            leftType = leftType.underlyingType;
9✔
1608
        }
1609
        if (isEnumMemberType(rightType)) {
663✔
1610
            rightType = rightType.underlyingType;
8✔
1611
        }
1612

1613
        // treat object type like dynamic
1614
        if (isObjectType(leftType)) {
663✔
1615
            leftType = DynamicType.instance;
4✔
1616
        }
1617
        if (isObjectType(rightType)) {
663!
1618
            rightType = DynamicType.instance;
×
1619
        }
1620

1621
        let hasDouble = isDoubleTypeLike(leftType) || isDoubleTypeLike(rightType);
663✔
1622
        let hasFloat = isFloatTypeLike(leftType) || isFloatTypeLike(rightType);
663✔
1623
        let hasLongInteger = isLongIntegerTypeLike(leftType) || isLongIntegerTypeLike(rightType);
663✔
1624
        let hasInvalid = isInvalidTypeLike(leftType) || isInvalidTypeLike(rightType);
663✔
1625
        let hasDynamic = isDynamicType(leftType) || isDynamicType(rightType);
663✔
1626
        let bothDynamic = isDynamicType(leftType) && isDynamicType(rightType);
663✔
1627
        let bothNumbers = isNumberType(leftType) && isNumberType(rightType);
663✔
1628
        let hasNumber = isNumberType(leftType) || isNumberType(rightType);
663✔
1629
        let bothStrings = isStringTypeLike(leftType) && isStringTypeLike(rightType);
663✔
1630
        let hasString = isStringTypeLike(leftType) || isStringTypeLike(rightType);
663✔
1631
        let hasBoolean = isBooleanTypeLike(leftType) || isBooleanTypeLike(rightType);
663✔
1632
        let eitherBooleanOrNum = (isNumberType(leftType) || isBooleanTypeLike(leftType)) && (isNumberType(rightType) || isBooleanTypeLike(rightType));
663✔
1633

1634
        let leftIsPrimitive = isPrimitiveType(leftType);
663✔
1635
        let rightIsPrimitive = isPrimitiveType(rightType);
663✔
1636
        let hasPrimitive = leftIsPrimitive || rightIsPrimitive;
663✔
1637

1638
        let nonDynamicType: BscType;
1639
        if (hasPrimitive) {
663✔
1640
            nonDynamicType = leftIsPrimitive ? leftType : rightType;
590✔
1641
        }
1642

1643
        // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
1644
        switch (operator.kind) {
663✔
1645
            // Math operators
1646
            case TokenKind.Plus:
2,059✔
1647
            case TokenKind.PlusEqual:
1648
                if (bothStrings) {
267✔
1649
                    // "string" + "string" is the only binary expression allowed with strings
1650
                    return StringType.instance;
121✔
1651
                } else if (hasString && hasDynamic) {
146✔
1652
                    // assume dynamicValue is a string
1653
                    return StringType.instance;
9✔
1654
                }
1655
            // eslint-disable-next-line no-fallthrough
1656
            case TokenKind.Minus:
1657
            case TokenKind.MinusEqual:
1658
            case TokenKind.Star:
1659
            case TokenKind.StarEqual:
1660
            case TokenKind.Mod:
1661
                if (bothNumbers) {
228✔
1662
                    if (hasDouble) {
158✔
1663
                        return DoubleType.instance;
6✔
1664
                    } else if (hasFloat) {
152✔
1665
                        return FloatType.instance;
29✔
1666

1667
                    } else if (hasLongInteger) {
123✔
1668
                        return LongIntegerType.instance;
16✔
1669
                    }
1670
                    return IntegerType.instance;
107✔
1671
                } else if (hasNumber && hasDynamic) {
70✔
1672
                    // assume dynamic is a number
1673
                    return nonDynamicType;
35✔
1674
                }
1675
                break;
35✔
1676
            case TokenKind.Forwardslash:
1677
            case TokenKind.ForwardslashEqual:
1678
                if (bothNumbers) {
15✔
1679
                    if (hasDouble) {
10✔
1680
                        return DoubleType.instance;
1✔
1681
                    } else if (hasFloat) {
9✔
1682
                        return FloatType.instance;
1✔
1683

1684
                    } else if (hasLongInteger) {
8✔
1685
                        return LongIntegerType.instance;
3✔
1686
                    }
1687
                    return FloatType.instance;
5✔
1688
                } else if (hasNumber && hasDynamic) {
5✔
1689
                    // assume dynamic is a number
1690
                    return nonDynamicType;
4✔
1691
                }
1692
                break;
1✔
1693
            case TokenKind.Backslash:
1694
            case TokenKind.BackslashEqual:
1695
                if (bothNumbers) {
14✔
1696
                    if (hasLongInteger) {
6✔
1697
                        return LongIntegerType.instance;
2✔
1698
                    }
1699
                    return IntegerType.instance;
4✔
1700
                } else if (hasNumber && hasDynamic) {
8✔
1701
                    // assume dynamic is a number
1702
                    return IntegerType.instance;
7✔
1703
                }
1704
                break;
1✔
1705
            case TokenKind.Caret:
1706
                if (bothNumbers) {
21✔
1707
                    if (hasDouble || hasLongInteger) {
18✔
1708
                        return DoubleType.instance;
4✔
1709
                    } else if (hasFloat) {
14✔
1710
                        return FloatType.instance;
2✔
1711
                    }
1712
                    return IntegerType.instance;
12✔
1713
                } else if (hasNumber && hasDynamic) {
3!
1714
                    // assume dynamic is a number
1715
                    return IntegerType.instance;
3✔
1716
                }
1717
                break;
×
1718
            // Bitshift operators
1719
            case TokenKind.LeftShift:
1720
            case TokenKind.LeftShiftEqual:
1721
            case TokenKind.RightShift:
1722
            case TokenKind.RightShiftEqual:
1723
                if (bothNumbers) {
28✔
1724
                    if (hasLongInteger) {
20✔
1725
                        return LongIntegerType.instance;
2✔
1726
                    }
1727
                    // Bitshifts are allowed with non-integer numerics
1728
                    // but will always truncate to ints
1729
                    return IntegerType.instance;
18✔
1730
                } else if (hasNumber && hasDynamic) {
8✔
1731
                    // assume dynamic is a number
1732
                    return IntegerType.instance;
6✔
1733
                }
1734
                break;
2✔
1735
            // Comparison operators
1736
            // All comparison operators result in boolean
1737
            case TokenKind.Equal:
1738
            case TokenKind.LessGreater:
1739
                // = and <> can accept invalid / dynamic
1740
                if (hasDynamic || hasInvalid || bothStrings || eitherBooleanOrNum) {
127✔
1741
                    return BooleanType.instance;
124✔
1742
                }
1743
                break;
3✔
1744
            case TokenKind.Greater:
1745
            case TokenKind.Less:
1746
            case TokenKind.GreaterEqual:
1747
            case TokenKind.LessEqual:
1748
                if (bothStrings || bothNumbers) {
38✔
1749
                    return BooleanType.instance;
19✔
1750
                } else if ((hasNumber || hasString) && hasDynamic) {
19!
1751
                    // assume dynamic is a valid type
1752
                    return BooleanType.instance;
18✔
1753
                }
1754
                break;
1✔
1755
            // Logical or bitwise operators
1756
            case TokenKind.Or:
1757
            case TokenKind.And:
1758
                if (bothNumbers) {
62✔
1759
                    // "and"/"or" represent bitwise operators
1760
                    if (hasLongInteger && !hasDouble && !hasFloat) {
12✔
1761
                        // 2 long ints or long int and int
1762
                        return LongIntegerType.instance;
1✔
1763
                    }
1764
                    return IntegerType.instance;
11✔
1765
                } else if (eitherBooleanOrNum) {
50✔
1766
                    // "and"/"or" represent logical operators
1767
                    return BooleanType.instance;
39✔
1768
                } else if (hasNumber && hasDynamic) {
11✔
1769
                    // assume dynamic is a valid type
1770
                    return IntegerType.instance;
7✔
1771
                } else if (hasBoolean && hasDynamic) {
4✔
1772
                    // assume dynamic is a valid type
1773
                    return BooleanType.instance;
1✔
1774
                }
1775
                break;
3✔
1776
        }
1777
        if (bothDynamic) {
46✔
1778
            return DynamicType.instance;
22✔
1779
        }
1780
        return undefined;
24✔
1781
    }
1782

1783
    public getHighestPriorityType(types: BscType[], depth = 0): BscType {
26✔
1784
        let result: BscType;
1785
        if (depth > 4) {
26!
1786
            // shortcut for very complicated types, or self-referencing union types
1787
            return DynamicType.instance;
×
1788
        }
1789
        for (let type of types) {
26✔
1790
            if (isUnionType(type)) {
54!
1791
                type = getUniqueType([type], unionTypeFactory);
×
1792
                if (isUnionType(type)) {
×
1793
                    type = this.getHighestPriorityType(type.types, depth + 1);
×
1794
                }
1795
            }
1796
            if (!result) {
54✔
1797
                result = type;
26✔
1798
            } else {
1799
                if (type.binaryOpPriorityLevel < result.binaryOpPriorityLevel) {
28✔
1800
                    result = type;
11✔
1801
                } else if (type.binaryOpPriorityLevel === result.binaryOpPriorityLevel && !result.isEqual(type)) {
17✔
1802
                    // equal priority types, but not equal types, like Boolean and String... just be dynamic at this point
1803
                    result = DynamicType.instance;
2✔
1804
                }
1805
            }
1806
            if (isUninitializedType(type)) {
54!
1807
                return type;
×
1808
            }
1809
            if (isVoidType(type)) {
54!
1810
                return type;
×
1811
            }
1812
            if (isInvalidTypeLike(type)) {
54!
1813
                return type;
×
1814
            }
1815
            if (isObjectType(type) && !isDynamicType(type)) {
54!
1816
                result = type;
×
1817
            }
1818
            if (isDynamicType(type)) {
54✔
1819
                result = type;
4✔
1820
            }
1821
        }
1822
        return result ?? DynamicType.instance;
26!
1823
    }
1824

1825
    /**
1826
     * Return the type of the result of a unary operator
1827
     */
1828
    public unaryOperatorResultType(operator: Token, exprType: BscType): BscType {
1829

1830
        if (isUnionType(exprType)) {
94✔
1831
            exprType = this.getHighestPriorityType(exprType.types);
2✔
1832
        }
1833

1834
        if (isVoidType(exprType) || isInvalidTypeLike(exprType) || isUninitializedType(exprType)) {
94✔
1835
            return undefined;
2✔
1836
        }
1837

1838

1839
        if (isDynamicType(exprType) || isObjectType(exprType)) {
92✔
1840
            return exprType;
7✔
1841
        }
1842

1843
        // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
1844
        switch (operator.kind) {
85✔
1845
            // Math operators
1846
            case TokenKind.Plus: // (`num = +num` is valid syntax)
88✔
1847
            case TokenKind.Minus:
1848
                if (isNumberType(exprType)) {
60✔
1849
                    // a negative number will be the same type, eg, double->double, int->int, etc.
1850
                    return this.getUnboxedType(exprType);
55✔
1851
                }
1852
                break;
5✔
1853
            case TokenKind.Not:
1854
                if (isBooleanTypeLike(exprType)) {
25✔
1855
                    return BooleanType.instance;
13✔
1856
                } else if (isNumberType(exprType)) {
12✔
1857
                    //numbers can be "notted"
1858
                    // by default they go to ints, except longints, which stay that way
1859
                    if (isLongIntegerTypeLike(exprType)) {
11✔
1860
                        return LongIntegerType.instance;
1✔
1861
                    }
1862
                    return IntegerType.instance;
10✔
1863
                }
1864
                break;
1✔
1865
        }
1866
        return undefined;
6✔
1867
    }
1868

1869
    public getUnboxedType(boxedType: BscType): BscType {
1870
        if (isIntegerTypeLike(boxedType)) {
55✔
1871
            return IntegerType.instance;
40✔
1872
        } else if (isLongIntegerTypeLike(boxedType)) {
15!
1873
            return LongIntegerType.instance;
×
1874
        } else if (isFloatTypeLike(boxedType)) {
15✔
1875
            return FloatType.instance;
14✔
1876
        } else if (isDoubleTypeLike(boxedType)) {
1!
1877
            return DoubleType.instance;
1✔
1878
        } else if (isBooleanTypeLike(boxedType)) {
×
1879
            return BooleanType.instance;
×
1880
        } else if (isStringTypeLike(boxedType)) {
×
1881
            return StringType.instance;
×
1882
        } else if (isInvalidTypeLike(boxedType)) {
×
1883
            return InvalidType.instance;
×
1884
        }
1885
        return boxedType;
×
1886
    }
1887

1888
    /**
1889
     * Get the extension for the given file path. Basically the part after the final dot, except for
1890
     * `d.bs` which is treated as single extension
1891
     * @returns the file extension (i.e. ".d.bs", ".bs", ".brs", ".xml", ".jpg", etc...)
1892
     */
1893
    public getExtension(filePath: string) {
1894
        filePath = filePath.toLowerCase();
2,923✔
1895
        if (filePath.endsWith('.d.bs')) {
2,923✔
1896
            return '.d.bs';
34✔
1897
        } else {
1898
            return path.extname(filePath).toLowerCase();
2,889✔
1899
        }
1900
    }
1901

1902
    /**
1903
     * Load and return the list of plugins
1904
     */
1905
    public loadPlugins(cwd: string, pathOrModules: string[], onError?: (pathOrModule: string, err: Error) => void): Plugin[] {
1906
        const logger = createLogger();
102✔
1907
        return pathOrModules.reduce<Plugin[]>((acc, pathOrModule) => {
102✔
1908
            if (typeof pathOrModule === 'string') {
8!
1909
                try {
8✔
1910
                    const loaded = requireRelative(pathOrModule, cwd);
8✔
1911
                    const theExport: Plugin | PluginFactory = loaded.default ? loaded.default : loaded;
8✔
1912

1913
                    let plugin: Plugin | undefined;
1914

1915
                    // legacy plugins returned a plugin object. If we find that, then add a warning
1916
                    if (typeof theExport === 'object') {
8✔
1917
                        logger.warn(`Plugin "${pathOrModule}" was loaded as a singleton. Please contact the plugin author to update to the factory pattern.\n`);
2✔
1918
                        plugin = theExport;
2✔
1919

1920
                        // the official plugin format is a factory function that returns a new instance of a plugin.
1921
                    } else if (typeof theExport === 'function') {
6!
1922
                        plugin = theExport({
6✔
1923
                            version: this.getBrighterScriptVersion()
1924
                        });
1925
                    } else {
1926
                        //this should never happen; somehow an invalid plugin has made it into here
1927
                        throw new Error(`TILT: Encountered an invalid plugin: ${String(plugin)}`);
×
1928
                    }
1929

1930
                    if (!plugin.name) {
8✔
1931
                        plugin.name = pathOrModule;
1✔
1932
                    }
1933
                    acc.push(plugin);
8✔
1934
                } catch (err: any) {
1935
                    if (onError) {
×
1936
                        onError(pathOrModule, err);
×
1937
                    } else {
1938
                        throw err;
×
1939
                    }
1940
                }
1941
            }
1942
            return acc;
8✔
1943
        }, []);
1944
    }
1945

1946
    /**
1947
     * Gathers expressions, variables, and unique names from an expression.
1948
     * This is mostly used for the ternary expression
1949
     */
1950
    public getExpressionInfo(expression: Expression, file: BrsFile): ExpressionInfo {
1951
        const expressions = [expression];
66✔
1952
        const variableExpressions = [] as VariableExpression[];
66✔
1953
        const uniqueVarNames = new Set<string>();
66✔
1954

1955
        function expressionWalker(expression) {
1956
            if (isExpression(expression)) {
170✔
1957
                expressions.push(expression);
166✔
1958
            }
1959
            if (isVariableExpression(expression)) {
170✔
1960
                variableExpressions.push(expression);
55✔
1961
                uniqueVarNames.add(expression.tokens.name.text);
55✔
1962
            }
1963
        }
1964

1965
        // Collect all expressions. Most of these expressions are fairly small so this should be quick!
1966
        // This should only be called during transpile time and only when we actually need it.
1967
        expression?.walk(expressionWalker, {
66✔
1968
            walkMode: WalkMode.visitExpressions
1969
        });
1970

1971
        //handle the expression itself (for situations when expression is a VariableExpression)
1972
        expressionWalker(expression);
66✔
1973

1974
        const scope = file.program.getFirstScopeForFile(file);
66✔
1975
        let filteredVarNames = [...uniqueVarNames];
66✔
1976
        if (scope) {
66!
1977
            filteredVarNames = filteredVarNames.filter((varName: string) => {
66✔
1978
                const varNameLower = varName.toLowerCase();
53✔
1979
                // TODO: include namespaces in this filter
1980
                return !scope.getEnumMap().has(varNameLower) &&
53✔
1981
                    !scope.getConstMap().has(varNameLower);
1982
            });
1983
        }
1984

1985
        return { expressions: expressions, varExpressions: variableExpressions, uniqueVarNames: filteredVarNames };
66✔
1986
    }
1987

1988

1989
    public concatAnnotationLeadingTrivia(stmt: Statement): Token[] {
1990
        return [...(stmt.annotations?.map(anno => anno.leadingTrivia ?? []).flat() ?? []), ...stmt.leadingTrivia];
168!
1991
    }
1992

1993
    /**
1994
     * Create a SourceNode that maps every line to itself. Useful for creating maps for files
1995
     * that haven't changed at all, but we still need the map
1996
     */
1997
    public simpleMap(source: string, src: string) {
1998
        //create a source map from the original source code
1999
        let chunks = [] as (SourceNode | string)[];
7✔
2000
        let lines = src.split(/\r?\n/g);
7✔
2001
        for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
7✔
2002
            let line = lines[lineIndex];
27✔
2003
            chunks.push(
27✔
2004
                lineIndex > 0 ? '\n' : '',
27✔
2005
                new SourceNode(lineIndex + 1, 0, source, line)
2006
            );
2007
        }
2008
        return new SourceNode(null, null, source, chunks);
7✔
2009
    }
2010

2011
    private isWindows = process.platform === 'win32';
1✔
2012
    private standardizePathCache = new Map<string, string>();
1✔
2013

2014
    /**
2015
     * Converts a path into a standardized format (drive letter to lower, remove extra slashes, use single slash type, resolve relative parts, etc...)
2016
     */
2017
    public standardizePath(thePath: string): string {
2018
        //if we have the value in cache already, return it
2019
        if (this.standardizePathCache.has(thePath)) {
33,484✔
2020
            return this.standardizePathCache.get(thePath);
30,655✔
2021
        }
2022
        const originalPath = thePath;
2,829✔
2023

2024
        if (typeof thePath !== 'string') {
2,829!
2025
            return thePath;
×
2026
        }
2027

2028
        //windows path.normalize will convert all slashes to backslashes and remove duplicates
2029
        if (this.isWindows) {
2,829✔
2030
            thePath = path.win32.normalize(thePath);
2,814✔
2031
        } else {
2032
            //replace all windows or consecutive slashes with path.sep
2033
            thePath = thePath.replace(/[\/\\]+/g, '/');
15✔
2034

2035
            // only use path.normalize if dots are present since it's expensive
2036
            if (thePath.includes('./')) {
15!
2037
                thePath = path.posix.normalize(thePath);
×
2038
            }
2039
        }
2040

2041
        // Lowercase drive letter on Windows-like paths (e.g., "C:/...")
2042
        if (thePath.charCodeAt(1) === 58 /* : */) {
2,829✔
2043
            // eslint-disable-next-line no-var
2044
            var firstChar = thePath.charCodeAt(0);
1,408✔
2045
            if (firstChar >= 65 && firstChar <= 90) {
1,408✔
2046
                thePath = String.fromCharCode(firstChar + 32) + thePath.slice(1);
324✔
2047
            }
2048
        }
2049
        this.standardizePathCache.set(originalPath, thePath);
2,829✔
2050
        return thePath;
2,829✔
2051
    }
2052

2053
    /**
2054
     * Given a Diagnostic or BsDiagnostic, return a deep clone of the diagnostic.
2055
     * @param diagnostic the diagnostic to clone
2056
     * @param relatedInformationFallbackLocation a default location to use for all `relatedInformation` entries that are missing a location
2057
     */
2058
    public toDiagnostic(diagnostic: Diagnostic | BsDiagnostic, relatedInformationFallbackLocation: string): Diagnostic {
2059
        let relatedInformation = diagnostic.relatedInformation ?? [];
137✔
2060
        if (relatedInformation.length > MAX_RELATED_INFOS_COUNT) {
137!
2061
            const relatedInfoLength = relatedInformation.length;
×
2062
            relatedInformation = relatedInformation.slice(0, MAX_RELATED_INFOS_COUNT);
×
2063
            relatedInformation.push({
×
2064
                message: `...and ${relatedInfoLength - MAX_RELATED_INFOS_COUNT} more`,
2065
                location: util.createLocationFromRange('   ', util.createRange(0, 0, 0, 0))
2066
            });
2067
        }
2068

2069
        const range = (diagnostic as BsDiagnostic).location?.range ??
137✔
2070
            (diagnostic as Diagnostic).range;
2071

2072
        let result = {
137✔
2073
            severity: diagnostic.severity,
2074
            range: range,
2075
            message: diagnostic.message,
2076
            relatedInformation: relatedInformation.map(x => {
2077

2078
                //clone related information just in case a plugin added circular ref info here
2079
                const clone = { ...x };
42✔
2080
                if (!clone.location) {
42✔
2081
                    // use the fallback location if available
2082
                    if (relatedInformationFallbackLocation) {
2✔
2083
                        clone.location = util.createLocationFromRange(relatedInformationFallbackLocation, range);
1✔
2084
                    } else {
2085
                        //remove this related information so it doesn't bring crash the language server
2086
                        return undefined;
1✔
2087
                    }
2088
                }
2089
                return clone;
41✔
2090
                //filter out null relatedInformation items
2091
            }).filter((x): x is DiagnosticRelatedInformation => Boolean(x)),
42✔
2092
            code: diagnostic.code ? diagnostic.code : (diagnostic as BsDiagnostic).legacyCode,
137!
2093
            source: diagnostic.source ?? 'brs'
411✔
2094
        } as Diagnostic;
2095
        if (diagnostic?.tags?.length > 0) {
137!
2096
            result.tags = diagnostic.tags;
×
2097
        }
2098
        return result;
137✔
2099
    }
2100

2101
    /**
2102
     * Get the first locatable item found at the specified position
2103
     * @param locatables an array of items that have a `range` property
2104
     * @param position the position that the locatable must contain
2105
     */
2106
    public getFirstLocatableAt(locatables: Locatable[], position: Position) {
2107
        for (let token of locatables) {
×
2108
            if (util.rangeContains(token.location?.range, position)) {
×
2109
                return token;
×
2110
            }
2111
        }
2112
    }
2113

2114
    /**
2115
     * Sort an array of objects that have a Range
2116
     */
2117
    public sortByRange<T extends { range: Range | undefined }>(locatables: T[]) {
2118
        //sort the tokens by range
2119
        return locatables?.sort((a, b) => {
30!
2120
            //handle undefined tokens to prevent crashes
2121
            if (!a?.range) {
248!
2122
                return 1;
1✔
2123
            }
2124
            if (!b?.range) {
247!
2125
                return -1;
×
2126
            }
2127

2128
            //start line
2129
            if (a.range.start.line < b.range.start.line) {
247!
2130
                return -1;
×
2131
            }
2132
            if (a.range.start.line > b.range.start.line) {
247✔
2133
                return 1;
156✔
2134
            }
2135
            //start char
2136
            if (a.range.start.character < b.range.start.character) {
91✔
2137
                return -1;
60✔
2138
            }
2139
            if (a.range.start.character > b.range.start.character) {
31!
2140
                return 1;
31✔
2141
            }
2142
            //end line
2143
            if (a.range.end.line < b.range.end.line) {
×
2144
                return -1;
×
2145
            }
2146
            if (a.range.end.line > b.range.end.line) {
×
2147
                return 1;
×
2148
            }
2149
            //end char
2150
            if (a.range.end.character < b.range.end.character) {
×
2151
                return -1;
×
2152
            } else if (a.range.end.character > b.range.end.character) {
×
2153
                return 1;
×
2154
            }
2155
            return 0;
×
2156
        });
2157
    }
2158

2159
    /**
2160
     * Split the given text and return ranges for each chunk.
2161
     * Only works for single-line strings
2162
     */
2163
    public splitGetRange(separator: string, text: string, range: Range) {
2164
        const chunks = text.split(separator);
3✔
2165
        const result = [] as Array<{ text: string; range: Range }>;
3✔
2166
        let offset = 0;
3✔
2167
        for (let chunk of chunks) {
3✔
2168
            //only keep nonzero chunks
2169
            if (chunk.length > 0) {
8✔
2170
                result.push({
7✔
2171
                    text: chunk,
2172
                    range: this.createRange(
2173
                        range.start.line,
2174
                        range.start.character + offset,
2175
                        range.end.line,
2176
                        range.start.character + offset + chunk.length
2177
                    )
2178
                });
2179
            }
2180
            offset += chunk.length + separator.length;
8✔
2181
        }
2182
        return result;
3✔
2183
    }
2184

2185
    /**
2186
     * Wrap the given code in a markdown code fence (with the language)
2187
     */
2188
    public mdFence(code: string, language = '') {
×
2189
        return '```' + language + '\n' + code + '\n```';
129✔
2190
    }
2191

2192
    /**
2193
     * Gets each part of the dotted get.
2194
     * @param node any ast expression
2195
     * @returns an array of the parts of the dotted get. If not fully a dotted get, then returns undefined
2196
     */
2197
    public getAllDottedGetParts(node: AstNode): Identifier[] | undefined {
2198
        //this is a hot function and has been optimized. Don't rewrite unless necessary
2199
        const parts: Identifier[] = [];
12,810✔
2200
        let nextPart = node;
12,810✔
2201
        loop: while (nextPart) {
12,810✔
2202
            switch (nextPart?.kind) {
19,021!
2203
                case AstNodeKind.AssignmentStatement:
19,021!
2204
                    return [(node as AssignmentStatement).tokens.name];
9✔
2205
                case AstNodeKind.DottedGetExpression:
2206
                    parts.push((nextPart as DottedGetExpression)?.tokens.name);
6,140!
2207
                    nextPart = (nextPart as DottedGetExpression).obj;
6,140✔
2208
                    continue;
6,140✔
2209
                case AstNodeKind.CallExpression:
2210
                    nextPart = (nextPart as CallExpression).callee;
39✔
2211
                    continue;
39✔
2212
                case AstNodeKind.TypeExpression:
2213
                    nextPart = (nextPart as TypeExpression).expression;
×
2214
                    continue;
×
2215
                case AstNodeKind.VariableExpression:
2216
                    parts.push((nextPart as VariableExpression)?.tokens.name);
12,693!
2217
                    break loop;
12,693✔
2218
                case AstNodeKind.LiteralExpression:
2219
                    parts.push((nextPart as LiteralExpression)?.tokens.value as Identifier);
7!
2220
                    break loop;
7✔
2221
                case AstNodeKind.IndexedGetExpression:
2222
                    nextPart = (nextPart as unknown as IndexedGetExpression).obj;
35✔
2223
                    continue;
35✔
2224
                case AstNodeKind.FunctionParameterExpression:
2225
                    return [(nextPart as FunctionParameterExpression).tokens.name];
7✔
2226
                case AstNodeKind.GroupingExpression:
2227
                    parts.push(createIdentifier('()', nextPart.location));
7✔
2228
                    break loop;
7✔
2229
                default:
2230
                    //we found a non-DottedGet expression, so return because this whole operation is invalid.
2231
                    return undefined;
84✔
2232
            }
2233
        }
2234
        return parts.reverse();
12,710✔
2235
    }
2236

2237
    /**
2238
     * Given an expression, return all the DottedGet name parts as a string.
2239
     * Mostly used to convert namespaced item full names to a strings
2240
     */
2241
    public getAllDottedGetPartsAsString(node: Expression | Statement, parseMode = ParseMode.BrighterScript): string {
2,159✔
2242
        //this is a hot function and has been optimized. Don't rewrite unless necessary
2243
        /* eslint-disable no-var */
2244
        var sep = parseMode === ParseMode.BrighterScript ? '.' : '_';
11,218✔
2245
        const parts = this.getAllDottedGetParts(node) ?? [];
11,218✔
2246
        var result = parts[0]?.text;
11,218✔
2247
        for (var i = 1; i < parts.length; i++) {
11,218✔
2248
            result += sep + parts[i].text;
5,101✔
2249
        }
2250
        return result;
11,218✔
2251
        /* eslint-enable no-var */
2252
    }
2253

2254
    public stringJoin(strings: string[], separator: string) {
2255
        // eslint-disable-next-line no-var
2256
        var result = strings[0] ?? '';
×
2257
        // eslint-disable-next-line no-var
2258
        for (var i = 1; i < strings.length; i++) {
×
2259
            result += separator + strings[i];
×
2260
        }
2261
        return result;
×
2262
    }
2263

2264
    /**
2265
     * Break an expression into each part.
2266
     */
2267
    public splitExpression(expression: Expression) {
2268
        const parts: Expression[] = [expression];
11,051✔
2269
        let nextPart = expression;
11,051✔
2270
        while (nextPart) {
11,051✔
2271
            if (isDottedGetExpression(nextPart) || isIndexedGetExpression(nextPart) || isXmlAttributeGetExpression(nextPart)) {
13,869✔
2272
                nextPart = nextPart.obj;
1,086✔
2273

2274
            } else if (isCallExpression(nextPart) || isCallfuncExpression(nextPart)) {
12,783✔
2275
                nextPart = nextPart.callee;
1,732✔
2276

2277
            } else if (isTypeExpression(nextPart)) {
11,051!
2278
                nextPart = nextPart.expression;
×
2279
            } else {
2280
                break;
11,051✔
2281
            }
2282
            parts.unshift(nextPart);
2,818✔
2283
        }
2284
        return parts;
11,051✔
2285
    }
2286

2287
    /**
2288
     * Break an expression into each part, and return any VariableExpression or DottedGet expresisons from left-to-right.
2289
     */
2290
    public getDottedGetPath(expression: Expression): [VariableExpression, ...DottedGetExpression[]] {
2291
        let parts: Expression[] = [];
×
2292
        let nextPart = expression;
×
2293
        loop: while (nextPart) {
×
2294
            switch (nextPart?.kind) {
×
2295
                case AstNodeKind.DottedGetExpression:
×
2296
                    parts.push(nextPart);
×
2297
                    nextPart = (nextPart as DottedGetExpression).obj;
×
2298
                    continue;
×
2299
                case AstNodeKind.IndexedGetExpression:
2300
                case AstNodeKind.XmlAttributeGetExpression:
2301
                    nextPart = (nextPart as IndexedGetExpression | XmlAttributeGetExpression).obj;
×
2302
                    parts = [];
×
2303
                    continue;
×
2304
                case AstNodeKind.CallExpression:
2305
                case AstNodeKind.CallfuncExpression:
2306
                    nextPart = (nextPart as CallExpression | CallfuncExpression).callee;
×
2307
                    parts = [];
×
2308
                    continue;
×
2309
                case AstNodeKind.NewExpression:
2310
                    nextPart = (nextPart as NewExpression).call.callee;
×
2311
                    parts = [];
×
2312
                    continue;
×
2313
                case AstNodeKind.TypeExpression:
2314
                    nextPart = (nextPart as TypeExpression).expression;
×
2315
                    continue;
×
2316
                case AstNodeKind.VariableExpression:
2317
                    parts.push(nextPart);
×
2318
                    break loop;
×
2319
                default:
2320
                    return [] as any;
×
2321
            }
2322
        }
2323
        return parts.reverse() as any;
×
2324
    }
2325

2326
    /**
2327
     * Returns an integer if valid, or undefined. Eliminates checking for NaN
2328
     */
2329
    public parseInt(value: any) {
2330
        const result = parseInt(value);
34✔
2331
        if (!isNaN(result)) {
34✔
2332
            return result;
29✔
2333
        } else {
2334
            return undefined;
5✔
2335
        }
2336
    }
2337

2338
    /**
2339
     * Converts a range to a string in the format 1:2-3:4
2340
     */
2341
    public rangeToString(range: Range) {
2342
        return `${range?.start?.line}:${range?.start?.character}-${range?.end?.line}:${range?.end?.character}`;
2,057✔
2343
    }
2344

2345
    public validateTooDeepFile(file: (BrsFile | XmlFile)) {
2346
        //find any files nested too deep
2347
        let destPath = file?.destPath?.toString();
2,285!
2348
        let rootFolder = destPath?.replace(/^pkg:/, '').split(/[\\\/]/)[0].toLowerCase();
2,285!
2349

2350
        if (isBrsFile(file) && rootFolder !== 'source') {
2,285✔
2351
            return;
352✔
2352
        }
2353

2354
        if (isXmlFile(file) && rootFolder !== 'components') {
1,933!
2355
            return;
×
2356
        }
2357

2358
        let fileDepth = this.getParentDirectoryCount(destPath);
1,933✔
2359
        if (fileDepth >= 8) {
1,933✔
2360
            file.program?.diagnostics.register({
3!
2361
                ...DiagnosticMessages.detectedTooDeepFileSource(fileDepth),
2362
                location: util.createLocationFromFileRange(file, this.createRange(0, 0, 0, Number.MAX_VALUE))
2363
            });
2364
        }
2365
    }
2366

2367
    /**
2368
     * Execute dispose for a series of disposable items
2369
     * @param disposables a list of functions or disposables
2370
     */
2371
    public applyDispose(disposables: DisposableLike[]) {
2372
        for (const disposable of disposables ?? []) {
6!
2373
            if (typeof disposable === 'function') {
12!
2374
                disposable();
12✔
2375
            } else {
2376
                disposable?.dispose?.();
×
2377
            }
2378
        }
2379
    }
2380

2381
    /**
2382
     * Race a series of promises, and return the first one that resolves AND matches the matcher function.
2383
     * If all of the promises reject, then this will emit an AggregatreError with all of the errors.
2384
     * If at least one promise resolves, then this will log all of the errors to the console
2385
     * If at least one promise resolves but none of them match the matcher, then this will return undefined.
2386
     * @param promises all of the promises to race
2387
     * @param matcher a function that should return true if this value should be kept. Returning any value other than true means `false`
2388
     * @returns the first resolved value that matches the matcher, or undefined if none of them match
2389
     */
2390
    public async promiseRaceMatch<T>(promises: MaybePromise<T>[], matcher: (value: T) => boolean) {
2391
        const workingPromises = [
33✔
2392
            ...promises
2393
        ];
2394

2395
        const results: Array<{ value: T; index: number } | { error: Error; index: number }> = [];
33✔
2396
        let returnValue: T;
2397

2398
        while (workingPromises.length > 0) {
33✔
2399
            //race the promises. If any of them resolve, evaluate it against the matcher. If that passes, return the value. otherwise, eliminate this promise and try again
2400
            const result = await Promise.race(
39✔
2401
                workingPromises.map((promise, i) => {
2402
                    return Promise.resolve(promise)
58✔
2403
                        .then(value => ({ value: value, index: i }))
50✔
2404
                        .catch(error => ({ error: error, index: i }));
7✔
2405
                })
2406
            );
2407
            results.push(result);
39✔
2408
            //if we got a value and it matches the matcher, return it
2409
            if ('value' in result && matcher?.(result.value) === true) {
39✔
2410
                returnValue = result.value;
29✔
2411
                break;
29✔
2412
            }
2413

2414
            //remove this non-matched (or errored) promise from the list and try again
2415
            workingPromises.splice(result.index, 1);
10✔
2416
        }
2417

2418
        const errors = (results as Array<{ error: Error }>)
33✔
2419
            .filter(x => 'error' in x)
39✔
2420
            .map(x => x.error);
4✔
2421

2422
        //if all of them crashed, then reject
2423
        if (promises.length > 0 && errors.length === promises.length) {
33✔
2424
            throw new AggregateError(errors, 'All requests failed. First error message: ' + errors[0].message);
1✔
2425
        } else {
2426
            //log all of the errors
2427
            for (const error of errors) {
32✔
2428
                console.error(error);
1✔
2429
            }
2430
        }
2431

2432
        //return the matched value, or undefined if there wasn't one
2433
        return returnValue;
32✔
2434
    }
2435

2436
    /**
2437
     * Wraps SourceNode's constructor to be compatible with the TranspileResult type
2438
     */
2439
    public sourceNodeFromTranspileResult(
2440
        line: number | null,
2441
        column: number | null,
2442
        source: string | null,
2443
        chunks?: string | SourceNode | TranspileResult,
2444
        name?: string
2445
    ): SourceNode {
2446
        // we can use a typecast rather than actually transforming the data because SourceNode
2447
        // accepts a more permissive type than its typedef states
2448
        return new SourceNode(line, column, source, chunks as any, name);
8,513✔
2449
    }
2450

2451
    /**
2452
     * Find the index of the last item in the array that matches.
2453
     */
2454
    public findLastIndex<T>(array: T[], matcher: (T) => boolean) {
2455
        for (let i = array.length - 1; i >= 0; i--) {
27✔
2456
            if (matcher(array[i])) {
24✔
2457
                return i;
16✔
2458
            }
2459
        }
2460
    }
2461

2462
    public processTypeChain(typeChain: TypeChainEntry[]): TypeChainProcessResult {
2463
        let fullChainName = '';
1,287✔
2464
        let fullErrorName = '';
1,287✔
2465
        let itemName = '';
1,287✔
2466
        let previousTypeName = '';
1,287✔
2467
        let parentTypeName = '';
1,287✔
2468
        let itemTypeKind = '';
1,287✔
2469
        let parentTypeKind = '';
1,287✔
2470
        let astNode: AstNode;
2471
        let errorLocation: Location;
2472
        let containsDynamic = false;
1,287✔
2473
        let continueResolvingAllItems = true;
1,287✔
2474
        let crossedCallFunc = false;
1,287✔
2475
        for (let i = 0; i < typeChain.length; i++) {
1,287✔
2476
            const chainItem = typeChain[i];
2,407✔
2477
            const dotSep = chainItem.separatorToken?.text ?? '.';
2,407!
2478
            if (i > 0) {
2,407✔
2479
                fullChainName += dotSep;
1,122✔
2480
            }
2481
            fullChainName += chainItem.name;
2,407✔
2482
            if (continueResolvingAllItems) {
2,407✔
2483
                parentTypeName = previousTypeName;
2,202✔
2484
                parentTypeKind = itemTypeKind;
2,202✔
2485
                fullErrorName = previousTypeName ? `${previousTypeName}${dotSep}${chainItem.name}` : chainItem.name;
2,202✔
2486
                itemTypeKind = (chainItem.type as any)?.kind;
2,202✔
2487

2488
                let typeString = chainItem.type?.toString();
2,202✔
2489
                let typeToFindStringFor = chainItem.type;
2,202✔
2490
                while (typeToFindStringFor) {
2,202✔
2491
                    if (isUnionType(chainItem.type)) {
2,186✔
2492
                        typeString = `(${typeToFindStringFor.toString()})`;
7✔
2493
                        break;
7✔
2494
                    } else if (isCallableType(typeToFindStringFor)) {
2,179✔
2495
                        if (isTypedFunctionType(typeToFindStringFor) && i < typeChain.length - 1) {
35✔
2496
                            typeToFindStringFor = typeToFindStringFor.returnType;
9✔
2497
                        } else {
2498
                            typeString = 'function';
26✔
2499
                            break;
26✔
2500
                        }
2501
                        parentTypeName = previousTypeName;
9✔
2502
                    } else if (isNamespaceType(typeToFindStringFor) && parentTypeName) {
2,144✔
2503
                        const chainItemTypeName = typeToFindStringFor.toString();
332✔
2504
                        typeString = parentTypeName + '.' + chainItemTypeName;
332✔
2505
                        if (chainItemTypeName.toLowerCase().startsWith(parentTypeName.toLowerCase())) {
332!
2506
                            // the following namespace already knows...
2507
                            typeString = chainItemTypeName;
332✔
2508
                        }
2509
                        break;
332✔
2510
                    } else {
2511
                        typeString = typeToFindStringFor?.toString();
1,812!
2512
                        break;
1,812✔
2513
                    }
2514
                }
2515

2516
                previousTypeName = typeString ?? '';
2,202✔
2517
                itemName = chainItem.name;
2,202✔
2518
                astNode = chainItem.astNode;
2,202✔
2519
                containsDynamic = containsDynamic || (isDynamicType(chainItem.type) && !isAnyReferenceType(chainItem.type));
2,202✔
2520
                crossedCallFunc = crossedCallFunc || chainItem.data?.isFromCallFunc;
2,202!
2521
                if (!chainItem.isResolved) {
2,202✔
2522
                    errorLocation = chainItem.location;
1,116✔
2523
                    continueResolvingAllItems = false;
1,116✔
2524
                }
2525
            }
2526
        }
2527
        return {
1,287✔
2528
            itemName: itemName,
2529
            itemTypeKind: itemTypeKind,
2530
            itemParentTypeName: parentTypeName,
2531
            itemParentTypeKind: parentTypeKind,
2532
            fullNameOfItem: fullErrorName,
2533
            fullChainName: fullChainName,
2534
            location: errorLocation,
2535
            containsDynamic: containsDynamic,
2536
            astNode: astNode,
2537
            crossedCallFunc: crossedCallFunc
2538
        };
2539
    }
2540

2541

2542
    public isInTypeExpression(expression: AstNode): boolean {
2543
        //TODO: this is much faster than node.findAncestor(), but may need to be updated for "complicated" type expressions
2544
        if (isTypeExpression(expression) ||
16,602✔
2545
            isTypeExpression(expression?.parent) ||
49,308!
2546
            isTypedArrayExpression(expression) ||
2547
            isTypedArrayExpression(expression?.parent)) {
37,992!
2548
            return true;
4,004✔
2549
        }
2550
        if (isBinaryExpression(expression?.parent)) {
12,598!
2551
            let currentExpr: AstNode = expression.parent;
2,496✔
2552
            while (isBinaryExpression(currentExpr) && currentExpr.tokens.operator.kind === TokenKind.Or) {
2,496✔
2553
                currentExpr = currentExpr.parent;
124✔
2554
            }
2555
            return isTypeExpression(currentExpr) || isTypedArrayExpression(currentExpr);
2,496✔
2556
        }
2557
        return false;
10,102✔
2558
    }
2559

2560
    public isGenericNodeType(type: BscType) {
2561
        if (isComponentType(type)) {
38✔
2562
            const lowerName = type.toString().toLowerCase();
35✔
2563
            if (lowerName === 'rosgnode' || lowerName === 'rosgnodenode') {
35✔
2564
                return true;
7✔
2565
            }
2566
        }
2567
        return false;
31✔
2568
    }
2569

2570

2571
    public hasAnyRequiredSymbolChanged(requiredSymbols: UnresolvedSymbol[], changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
2572
        if (!requiredSymbols || !changedSymbols) {
1,984!
2573
            return false;
×
2574
        }
2575
        const runTimeChanges = changedSymbols.get(SymbolTypeFlag.runtime);
1,984✔
2576
        const typeTimeChanges = changedSymbols.get(SymbolTypeFlag.typetime);
1,984✔
2577

2578
        for (const symbol of requiredSymbols) {
1,984✔
2579
            if (this.setContainsUnresolvedSymbol(runTimeChanges, symbol) || this.setContainsUnresolvedSymbol(typeTimeChanges, symbol)) {
573✔
2580
                return true;
421✔
2581
            }
2582
        }
2583

2584
        return false;
1,563✔
2585
    }
2586

2587
    public setContainsUnresolvedSymbol(symbolLowerNameSet: Set<string>, symbol: UnresolvedSymbol) {
2588
        if (!symbolLowerNameSet || symbolLowerNameSet.size === 0) {
800✔
2589
            return false;
149✔
2590
        }
2591

2592
        for (const possibleNameLower of symbol.lookups) {
651✔
2593
            if (symbolLowerNameSet.has(possibleNameLower)) {
1,681✔
2594
                return true;
421✔
2595
            }
2596
        }
2597
        return false;
230✔
2598
    }
2599

2600
    public getCustomTypesInSymbolTree(setToFill: Set<BscType>, type: BscType, filter?: (t: BscSymbol) => boolean) {
2601
        const subSymbols = type.getMemberTable()?.getAllSymbols(SymbolTypeFlag.runtime) ?? [];
1,105!
2602
        for (const subSymbol of subSymbols) {
1,105✔
2603
            if (!subSymbol.type?.isBuiltIn && !setToFill.has(subSymbol.type)) {
4,748!
2604
                if (filter && !filter(subSymbol)) {
390!
2605
                    continue;
×
2606
                }
2607
                // if this is a custom type, and we haven't added it to the types to check to see if can add it to the additional types
2608
                // add the type, and investigate any members
2609
                setToFill.add(subSymbol.type);
390✔
2610
                this.getCustomTypesInSymbolTree(setToFill, subSymbol.type, filter);
390✔
2611
            }
2612

2613
        }
2614
    }
2615

2616
    public truncate<T>(options: {
2617
        leadingText: string;
2618
        items: T[];
2619
        trailingText?: string;
2620
        maxLength: number;
2621
        itemSeparator?: string;
2622
        partBuilder?: (item: T) => string;
2623
    }): string {
2624
        let leadingText = options.leadingText;
21✔
2625
        let items = options?.items ?? [];
21!
2626
        let trailingText = options?.trailingText ?? '';
21!
2627
        let maxLength = options?.maxLength ?? 160;
21!
2628
        let itemSeparator = options?.itemSeparator ?? ', ';
21!
2629
        let partBuilder = options?.partBuilder ?? ((x) => x.toString());
21!
2630

2631
        let parts = [];
21✔
2632
        let length = leadingText.length + (trailingText?.length ?? 0);
21!
2633

2634
        //calculate the max number of items we could fit in the given space
2635
        for (let i = 0; i < items.length; i++) {
21✔
2636
            let part = partBuilder(items[i]);
93✔
2637
            if (i > 0) {
93✔
2638
                part = itemSeparator + part;
72✔
2639
            }
2640
            parts.push(part);
93✔
2641
            length += part.length;
93✔
2642
            //exit the loop if we've maxed out our length
2643
            if (length >= maxLength) {
93✔
2644
                break;
6✔
2645
            }
2646
        }
2647
        let message: string;
2648
        //we have enough space to include all the parts
2649
        if (parts.length >= items.length) {
21✔
2650
            message = leadingText + parts.join('') + trailingText;
15✔
2651

2652
            //we require truncation
2653
        } else {
2654
            //account for truncation message length including max possible "more" items digits, trailing text length, and the separator between last item and trailing text
2655
            length = leadingText.length + `...and ${items.length} more`.length + itemSeparator.length + (trailingText?.length ?? 0);
6!
2656
            message = leadingText;
6✔
2657
            for (let i = 0; i < parts.length; i++) {
6✔
2658
                //always include at least 2 items. if this part would overflow the max, then skip it and finalize the message
2659
                if (i > 1 && length + parts[i].length > maxLength) {
47✔
2660
                    message += itemSeparator + `...and ${items.length - i} more` + trailingText;
6✔
2661
                    return message;
6✔
2662
                } else {
2663
                    message += parts[i];
41✔
2664
                    length += parts[i].length;
41✔
2665
                }
2666
            }
2667
        }
2668
        return message;
15✔
2669
    }
2670

2671
    public getAstNodeFriendlyName(node: AstNode) {
2672
        return node?.kind.replace(/Statement|Expression/g, '');
235!
2673
    }
2674

2675

2676
    public hasLeadingComments(input: Token | AstNode) {
2677
        const leadingTrivia = isToken(input) ? input?.leadingTrivia : input?.leadingTrivia ?? [];
7,735!
2678
        return !!leadingTrivia.find(t => t.kind === TokenKind.Comment);
15,716✔
2679
    }
2680

2681
    public getLeadingComments(input: Token | AstNode) {
2682
        const leadingTrivia = isToken(input) ? input?.leadingTrivia : input?.leadingTrivia ?? [];
12,276!
2683
        return leadingTrivia.filter(t => t.kind === TokenKind.Comment);
37,475✔
2684
    }
2685

2686
    public isLeadingCommentOnSameLine(line: RangeLike, input: Token | AstNode) {
2687
        const leadingCommentRange = this.getLeadingComments(input)?.[0];
11,399!
2688
        if (leadingCommentRange) {
11,399✔
2689
            return this.linesTouch(line, leadingCommentRange?.location);
1,636!
2690
        }
2691
        return false;
9,763✔
2692
    }
2693

2694
    public isClassUsedAsFunction(potentialClassType: BscType, expression: Expression, options: GetTypeOptions) {
2695
        // eslint-disable-next-line no-bitwise
2696
        if ((options?.flags ?? 0) & SymbolTypeFlag.runtime &&
26,426!
2697
            isClassType(potentialClassType) &&
2698
            !options.isExistenceTest &&
2699
            potentialClassType.name?.toLowerCase() === this.getAllDottedGetPartsAsString(expression)?.toLowerCase() &&
6,630✔
2700
            !expression?.findAncestor(isNewExpression)) {
1,314✔
2701
            return true;
32✔
2702
        }
2703
        return false;
26,394✔
2704
    }
2705

2706
    public getSpecialCaseCallExpressionReturnType(callExpr: CallExpression, options: GetSymbolTypeOptions) {
2707
        if (isVariableExpression(callExpr.callee) && callExpr.callee.tokens.name.text.toLowerCase() === 'createobject') {
700✔
2708
            const componentName = isLiteralString(callExpr.args[0]) ? callExpr.args[0].tokens.value?.text?.replace(/"/g, '') : '';
136!
2709
            const nodeType = componentName.toLowerCase() === 'rosgnode' && isLiteralString(callExpr.args[1]) ? callExpr.args[1].tokens.value?.text?.replace(/"/g, '') : '';
136!
2710
            if (componentName?.toLowerCase().startsWith('ro')) {
136!
2711
                let fullName = componentName + nodeType;
120✔
2712

2713
                if (nodeType.includes(':')) {
120✔
2714
                    // This node has a colon in its name, most likely from a component Library
2715
                    // This componentType is most likely unknown, so return `roSGNode`
2716
                    fullName = 'roSGNode';
8✔
2717
                }
2718

2719
                const data = {};
120✔
2720
                const symbolTable = callExpr.getSymbolTable();
120✔
2721
                const foundType = symbolTable.getSymbolType(fullName, {
120✔
2722
                    flags: SymbolTypeFlag.typetime,
2723
                    data: data,
2724
                    tableProvider: () => callExpr?.getSymbolTable(),
20!
2725
                    fullName: fullName
2726
                });
2727
                if (foundType) {
120!
2728
                    return foundType;
120✔
2729
                }
2730
            }
2731
        } else if (isDottedGetExpression(callExpr.callee) &&
564✔
2732
            callExpr.callee.tokens?.name?.text?.toLowerCase() === 'callfunc' &&
3,744!
2733
            isLiteralString(callExpr.args?.[0])) {
6!
2734
            return this.getCallFuncType(callExpr, callExpr.args?.[0]?.tokens.value, options);
2!
2735
        } else if (isDottedGetExpression(callExpr.callee) &&
562✔
2736
            callExpr.callee.tokens?.name?.text?.toLowerCase() === 'createchild' &&
3,726!
2737
            isComponentType(callExpr.callee.obj?.getType({ flags: SymbolTypeFlag.runtime })) &&
6!
2738
            isLiteralString(callExpr.args?.[0])) {
6!
2739
            const fullName = `roSGNode${callExpr.args?.[0].tokens?.value?.text?.replace(/"/g, '')}`;
2!
2740
            const data = {};
2✔
2741
            const symbolTable = callExpr.getSymbolTable();
2✔
2742
            return symbolTable.getSymbolType(fullName, {
2✔
2743
                flags: SymbolTypeFlag.typetime,
2744
                data: data,
2745
                tableProvider: () => callExpr?.getSymbolTable(),
×
2746
                fullName: fullName
2747
            });
2748
        }
2749
    }
2750

2751
    public getCallFuncType(callExpr: CallExpression | CallfuncExpression, methodNameToken: Token | Identifier, options: GetSymbolTypeOptions) {
2752
        let result: BscType;
2753
        let methodName = methodNameToken?.text?.replace(/"/g, ''); // remove quotes if it was the first arg of .callFunc()
53✔
2754

2755
        // a little hacky here with checking options.ignoreCall because callFuncExpression has the method name
2756
        // It's nicer for CallExpression, because it's a call on any expression.
2757
        let calleeType: BscType;
2758
        if (isCallfuncExpression(callExpr)) {
53✔
2759
            calleeType = callExpr.callee.getType({ ...options, flags: SymbolTypeFlag.runtime, ignoreCall: false });
44✔
2760
        } else if (isCallExpression(callExpr) && isDottedGetExpression(callExpr.callee)) {
9!
2761
            calleeType = callExpr.callee.obj.getType({ ...options, flags: SymbolTypeFlag.runtime, ignoreCall: false });
9✔
2762
        }
2763
        if (isComponentType(calleeType) || isReferenceType(calleeType)) {
53!
2764
            const funcType = (calleeType as ComponentType).getCallFuncType?.(methodName, options);
53!
2765
            if (funcType) {
50✔
2766
                options.typeChain?.push(new TypeChainEntry({
44✔
2767
                    name: methodName,
2768
                    type: funcType,
2769
                    data: options.data,
2770
                    location: methodNameToken.location,
2771
                    separatorToken: createToken(TokenKind.Callfunc),
2772
                    astNode: callExpr
2773
                }));
2774
                if (options.ignoreCall) {
44✔
2775
                    result = funcType;
23✔
2776
                } else if (isCallableType(funcType) && (!isReferenceType(funcType.returnType) || funcType.returnType.isResolvable())) {
21✔
2777
                    result = funcType.returnType;
13✔
2778
                } else if (!isReferenceType(funcType) && (funcType as any)?.returnType?.isResolvable()) {
8!
2779
                    result = (funcType as any).returnType;
×
2780
                } else {
2781
                    result = new TypePropertyReferenceType(funcType, 'returnType');
8✔
2782
                }
2783
            }
2784
        }
2785
        if (isVoidType(result)) {
50✔
2786
            // CallFunc will always return invalid, even if function called is `as void`
2787
            result = DynamicType.instance;
1✔
2788
        }
2789
        if (options.data && !options.ignoreCall) {
50✔
2790
            options.data.isFromCallFunc = true;
23✔
2791
        }
2792
        return result;
50✔
2793
    }
2794

2795
    public symbolComesFromSameNode(symbolName: string, definingNode: AstNode, symbolTable: SymbolTable) {
2796
        let nsData: ExtraSymbolData = {};
663✔
2797
        let foundType = symbolTable?.getSymbolType(symbolName, { flags: SymbolTypeFlag.runtime, data: nsData });
663!
2798
        if (foundType && definingNode === nsData?.definingNode) {
663!
2799
            return true;
264✔
2800
        }
2801
        return false;
399✔
2802
    }
2803

2804
    public isCalleeMemberOfNamespace(symbolName: string, nodeWhereUsed: AstNode, namespace?: NamespaceStatement) {
2805
        namespace = namespace ?? nodeWhereUsed.findAncestor<NamespaceStatement>(isNamespaceStatement);
72!
2806

2807
        if (!this.isVariableMemberOfNamespace(symbolName, nodeWhereUsed, namespace)) {
72✔
2808
            return false;
55✔
2809
        }
2810
        const exprType = nodeWhereUsed.getType({ flags: SymbolTypeFlag.runtime });
17✔
2811

2812
        if (isCallableType(exprType) || isClassType(exprType)) {
17!
2813
            return true;
17✔
2814
        }
2815
        return false;
×
2816
    }
2817

2818
    public isVariableMemberOfNamespace(symbolName: string, nodeWhereUsed: AstNode, namespace?: NamespaceStatement) {
2819
        namespace = namespace ?? nodeWhereUsed.findAncestor<NamespaceStatement>(isNamespaceStatement);
1,957✔
2820
        if (!isNamespaceStatement(namespace)) {
1,957✔
2821
            return false;
1,564✔
2822
        }
2823
        const namespaceParts = namespace.getNameParts();
393✔
2824
        let namespaceType: NamespaceType;
2825
        let symbolTable: SymbolTable = namespace.getSymbolTable();
393✔
2826
        for (const part of namespaceParts) {
393✔
2827
            namespaceType = symbolTable.getSymbolType(part.text, { flags: SymbolTypeFlag.runtime }) as NamespaceType;
678✔
2828
            if (namespaceType) {
678✔
2829
                symbolTable = namespaceType.getMemberTable();
677✔
2830
            } else {
2831
                return false;
1✔
2832
            }
2833
        }
2834

2835
        let varData: ExtraSymbolData = {};
392✔
2836
        nodeWhereUsed.getType({ flags: SymbolTypeFlag.runtime, data: varData });
392✔
2837
        const isFromSameNodeInMemberTable = this.symbolComesFromSameNode(symbolName, varData?.definingNode, namespaceType?.getMemberTable());
392!
2838
        return isFromSameNodeInMemberTable;
392✔
2839
    }
2840

2841
    public isVariableShadowingSomething(symbolName: string, nodeWhereUsed: AstNode) {
2842
        let varData: ExtraSymbolData = {};
6,749✔
2843
        let exprType = nodeWhereUsed.getType({ flags: SymbolTypeFlag.runtime, data: varData });
6,749✔
2844
        if (isReferenceType(exprType)) {
6,749✔
2845
            exprType = (exprType as any).getTarget();
6,299✔
2846
        }
2847
        const namespace = nodeWhereUsed?.findAncestor<NamespaceStatement>(isNamespaceStatement);
6,749!
2848

2849
        if (isNamespaceStatement(namespace)) {
6,749✔
2850
            let namespaceHasSymbol = namespace.getSymbolTable().hasSymbol(symbolName, SymbolTypeFlag.runtime);
79✔
2851
            // check if the namespace has a symbol with the same name, but different definiton
2852
            if (namespaceHasSymbol && !this.symbolComesFromSameNode(symbolName, varData.definingNode, namespace.getSymbolTable())) {
79✔
2853
                return true;
15✔
2854
            }
2855
        }
2856
        const bodyTable = nodeWhereUsed.getRoot().getSymbolTable();
6,734✔
2857
        const hasSymbolAtFileLevel = bodyTable.hasSymbol(symbolName, SymbolTypeFlag.runtime);
6,734✔
2858
        if (hasSymbolAtFileLevel && !this.symbolComesFromSameNode(symbolName, varData.definingNode, bodyTable)) {
6,734✔
2859
            return true;
10✔
2860
        }
2861

2862
        return false;
6,724✔
2863
    }
2864

2865
    public chooseTypeFromCodeOrDocComment(codeType: BscType, docType: BscType, options: GetTypeOptions) {
2866
        let returnType: BscType;
2867
        if (options.preferDocType && docType) {
11,893!
2868
            returnType = docType;
×
2869
            if (options.data) {
×
2870
                options.data.isFromDocComment = true;
×
2871
            }
2872
        } else {
2873
            returnType = codeType;
11,893✔
2874
            if (!returnType && docType) {
11,893✔
2875
                returnType = docType;
99✔
2876
                if (options.data) {
99✔
2877
                    options.data.isFromDocComment = true;
27✔
2878
                }
2879
            }
2880
        }
2881
        return returnType;
11,893✔
2882
    }
2883

2884
    /**
2885
     * Gets the type for a default value (eg. as a function param, class member or typed array)
2886
     */
2887
    public getDefaultTypeFromValueType(valueType: (BscType | BscType[])) {
2888
        if (!valueType) {
3,387✔
2889
            return undefined;
1,051✔
2890
        }
2891
        let resultType: BscType = DynamicType.instance;
2,336✔
2892
        if (Array.isArray(valueType)) {
2,336!
2893
            // passed an array opf types, potential from ArrayType.innerTypes
2894
            if (valueType.length > 0) {
×
2895
                //at least one, use it
2896
                resultType = valueType[0];
×
2897
                if (valueType?.length > 1) {
×
2898
                    // more than 1, find union
2899
                    resultType = getUniqueType(valueType, unionTypeFactory);
×
2900
                }
2901
            }
2902
        } else {
2903
            resultType = valueType;
2,336✔
2904
        }
2905
        if (!resultType.isResolvable()) {
2,336✔
2906
            if (isUnionType(resultType)) {
448✔
2907
                resultType = DynamicType.instance;
5✔
2908
            } else {
2909
                resultType = new ParamTypeFromValueReferenceType(resultType);
443✔
2910
            }
2911

2912
        } else if (isEnumMemberType(resultType)) {
1,888✔
2913
            // the type was an enum member... Try to get the parent enum type
2914
            resultType = resultType.parentEnumType ?? resultType;
24!
2915
        } else if (isUnionType(resultType)) {
1,864✔
2916
            // it was a union -- I wonder if they're resolvable now?
2917
            const moddedTypes = resultType.types.map(t => {
7✔
2918
                if (isEnumMemberType(t)) {
15✔
2919
                    // the type was an enum member... Try to get the parent enum type
2920
                    return t.parentEnumType ?? resultType;
6!
2921
                }
2922
                return t;
9✔
2923
            });
2924
            resultType = getUniqueType(moddedTypes, unionTypeFactory);
7✔
2925
        }
2926
        return resultType;
2,336✔
2927
    }
2928
}
2929

2930
/**
2931
 * A tagged template literal function for standardizing the path. This has to be defined as standalone function since it's a tagged template literal function,
2932
 * we can't use `object.tag` syntax.
2933
 */
2934
export function standardizePath(stringParts, ...expressions: any[]) {
1✔
2935
    let result: string[] = [];
25,624✔
2936
    for (let i = 0; i < stringParts.length; i++) {
25,624✔
2937
        result.push(stringParts[i], expressions[i]);
225,327✔
2938
    }
2939
    return util.standardizePath(
25,624✔
2940
        result.join('')
2941
    );
2942
}
2943

2944
/**
2945
 * An item that can be coerced into a `Range`
2946
 */
2947
export type RangeLike = { location?: Location } | Location | { range?: Range } | Range | undefined;
2948

2949
export let util = new Util();
1✔
2950
export default util;
1✔
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