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

rokucommunity / brighterscript / #15035

15 Dec 2025 08:42PM UTC coverage: 86.889%. Remained the same
#15035

push

web-flow
Merge a60226157 into 2ea4d2108

14466 of 17575 branches covered (82.31%)

Branch coverage included in aggregate %.

113 of 217 new or added lines in 8 files covered. (52.07%)

116 existing lines in 6 files now uncovered.

15185 of 16550 relevant lines covered (91.75%)

24075.2 hits per line

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

87.91
/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 type { BsConfig, FinalizedBsConfig } from './BsConfig';
11
import { DiagnosticMessages } from './DiagnosticMessages';
1✔
12
import type { CallableContainer, BsDiagnostic, FileReference, CallableContainerMap, Plugin, ExpressionInfo, TranspileResult, MaybePromise, DisposableLike, ExtraSymbolData, GetTypeOptions, TypeChainProcessResult, PluginFactory, TypeCircularReferenceInfo } from './interfaces';
13
import { TypeChainEntry } from './interfaces';
1✔
14
import { BooleanType } from './types/BooleanType';
1✔
15
import { DoubleType } from './types/DoubleType';
1✔
16
import { DynamicType } from './types/DynamicType';
1✔
17
import { FloatType } from './types/FloatType';
1✔
18
import { IntegerType } from './types/IntegerType';
1✔
19
import { LongIntegerType } from './types/LongIntegerType';
1✔
20
import { ObjectType } from './types/ObjectType';
1✔
21
import { StringType } from './types/StringType';
1✔
22
import { VoidType } from './types/VoidType';
1✔
23
import { ParseMode } from './parser/Parser';
1✔
24
import type { CallExpression, CallfuncExpression, DottedGetExpression, FunctionParameterExpression, IndexedGetExpression, LiteralExpression, TypeExpression, VariableExpression } from './parser/Expression';
25
import { LogLevel, createLogger } from './logging';
1✔
26
import { isToken, type Identifier, type Token } from './lexer/Token';
1✔
27
import { TokenKind } from './lexer/TokenKind';
1✔
28
import { isAnyReferenceType, isBinaryExpression, isBooleanTypeLike, isBrsFile, isCallExpression, isCallableType, isCallfuncExpression, isClassType, isComponentType, isDottedGetExpression, isDoubleTypeLike, isDynamicType, isEnumMemberType, isExpression, isFloatTypeLike, isIndexedGetExpression, isIntegerTypeLike, isIntersectionType, isInvalidTypeLike, isLiteralString, isLongIntegerTypeLike, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberTypeLike, isObjectType, isPrimitiveType, isReferenceType, isStatement, isStringTypeLike, isTypeExpression, isTypedArrayExpression, isTypedFunctionType, isUninitializedType, isUnionType, isVariableExpression, isVoidType, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection';
1✔
29
import { WalkMode } from './astUtils/visitors';
1✔
30
import { SourceNode } from 'source-map';
1✔
31
import * as requireRelative from 'require-relative';
1✔
32
import type { BrsFile } from './files/BrsFile';
33
import type { XmlFile } from './files/XmlFile';
34
import type { AstNode, Expression, Statement } from './parser/AstNode';
35
import { AstNodeKind } from './parser/AstNode';
1✔
36
import type { UnresolvedSymbol } from './AstValidationSegmenter';
37
import type { BscSymbol, GetSymbolTypeOptions, SymbolTable } from './SymbolTable';
38
import { SymbolTypeFlag } from './SymbolTypeFlag';
1✔
39
import { createIdentifier, createToken } from './astUtils/creators';
1✔
40
import { MAX_RELATED_INFOS_COUNT } from './diagnosticUtils';
1✔
41
import type { BscType } from './types/BscType';
42
import { UnionType, unionTypeFactory } from './types/UnionType';
1✔
43
import { ArrayType } from './types/ArrayType';
1✔
44
import { BinaryOperatorReferenceType, TypePropertyReferenceType, ParamTypeFromValueReferenceType } from './types/ReferenceType';
1✔
45
import { AssociativeArrayType } from './types/AssociativeArrayType';
1✔
46
import { ComponentType } from './types/ComponentType';
1✔
47
import { FunctionType } from './types/FunctionType';
1✔
48
import type { AssignmentStatement, NamespaceStatement } from './parser/Statement';
49
import type { BscFile } from './files/BscFile';
50
import type { NamespaceType } from './types/NamespaceType';
51
import { getUniqueType } from './types/helpers';
1✔
52
import { InvalidType } from './types/InvalidType';
1✔
53
import { TypedFunctionType } from './types';
1✔
54
import { IntersectionType } from './types/IntersectionType';
1✔
55

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

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

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

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

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

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

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

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

139
    /**
140
     * Find the path to the config file.
141
     * If the config file path doesn't exist
142
     * @param cwd the current working directory where the search for configs should begin
143
     */
144
    public getConfigFilePath(cwd?: string) {
145
        cwd = cwd ?? process.cwd();
101✔
146
        let configPath = path.join(cwd, 'bsconfig.json');
101✔
147
        //find the nearest config file path
148
        for (let i = 0; i < 100; i++) {
101✔
149
            if (this.pathExistsSync(configPath)) {
9,507✔
150
                return configPath;
6✔
151
            } else {
152
                let parentDirPath = path.dirname(path.dirname(configPath));
9,501✔
153
                configPath = path.join(parentDirPath, 'bsconfig.json');
9,501✔
154
            }
155
        }
156
    }
157

158
    public getRangeFromOffsetLength(text: string, offset: number, length: number) {
159
        let lineIndex = 0;
2✔
160
        let colIndex = 0;
2✔
161
        for (let i = 0; i < text.length; i++) {
2✔
162
            if (offset === i) {
158!
UNCOV
163
                break;
×
164
            }
165
            let char = text[i];
158✔
166
            if (char === '\n' || (char === '\r' && text[i + 1] === '\n')) {
158!
167
                lineIndex++;
4✔
168
                colIndex = 0;
4✔
169
                i++;
4✔
170
                continue;
4✔
171
            } else {
172
                colIndex++;
154✔
173
            }
174
        }
175
        return util.createRange(lineIndex, colIndex, lineIndex, colIndex + length);
2✔
176
    }
177

178
    /**
179
     * Load the contents of a config file.
180
     * If the file extends another config, this will load the base config as well.
181
     * @param configFilePath the relative or absolute path to a brighterscript config json file
182
     * @param parentProjectPaths a list of parent config files. This is used by this method to recursively build the config list
183
     */
184
    public loadConfigFile(configFilePath: string | undefined, parentProjectPaths?: string[], cwd = process.cwd()): BsConfig | undefined {
12✔
185
        if (configFilePath) {
77✔
186
            //if the config file path starts with question mark, then it's optional. return undefined if it doesn't exist
187
            if (configFilePath.startsWith('?')) {
76✔
188
                //remove leading question mark
189
                configFilePath = configFilePath.substring(1);
1✔
190
                if (fsExtra.pathExistsSync(path.resolve(cwd, configFilePath)) === false) {
1!
191
                    return undefined;
1✔
192
                }
193
            }
194
            //keep track of the inheritance chain
195
            parentProjectPaths = parentProjectPaths ? parentProjectPaths : [];
75✔
196
            configFilePath = path.resolve(cwd, configFilePath);
75✔
197
            if (parentProjectPaths?.includes(configFilePath)) {
75!
198
                parentProjectPaths.push(configFilePath);
1✔
199
                parentProjectPaths.reverse();
1✔
200
                throw new Error('Circular dependency detected: "' + parentProjectPaths.join('" => ') + '"');
1✔
201
            }
202
            //load the project file
203
            let projectFileContents = fsExtra.readFileSync(configFilePath).toString();
74✔
204
            let parseErrors = [] as ParseError[];
74✔
205
            let projectConfig = parseJsonc(projectFileContents, parseErrors, {
74✔
206
                allowEmptyContent: true,
207
                allowTrailingComma: true,
208
                disallowComments: false
209
            }) as BsConfig ?? {};
74✔
210
            if (parseErrors.length > 0) {
74✔
211
                let err = parseErrors[0];
2✔
212
                let diagnostic = {
2✔
213
                    ...DiagnosticMessages.syntaxError(`Syntax errors in bsconfig.json: ${printParseErrorCode(parseErrors[0].error)}`),
214
                    location: {
215
                        uri: this.pathToUri(configFilePath),
216
                        range: this.getRangeFromOffsetLength(projectFileContents, err.offset, err.length)
217
                    }
218
                } as BsDiagnostic;
219
                throw diagnostic; //eslint-disable-line @typescript-eslint/no-throw-literal
2✔
220
            }
221

222
            let projectFileCwd = path.dirname(configFilePath);
72✔
223

224
            //`plugins` paths should be relative to the current bsconfig
225
            this.resolvePathsRelativeTo(projectConfig, 'plugins', projectFileCwd);
72✔
226

227
            //`require` paths should be relative to cwd
228
            util.resolvePathsRelativeTo(projectConfig, 'require', projectFileCwd);
72✔
229

230
            let result: BsConfig;
231
            //if the project has a base file, load it
232
            if (projectConfig && typeof projectConfig.extends === 'string') {
72✔
233
                let baseProjectConfig = this.loadConfigFile(projectConfig.extends, [...parentProjectPaths, configFilePath], projectFileCwd);
9✔
234
                //extend the base config with the current project settings
235
                result = { ...baseProjectConfig, ...projectConfig };
7✔
236
            } else {
237
                result = projectConfig;
63✔
238
                let ancestors = parentProjectPaths ? parentProjectPaths : [];
63!
239
                ancestors.push(configFilePath);
63✔
240
                (result as any)._ancestors = parentProjectPaths;
63✔
241
            }
242

243
            //make any paths in the config absolute (relative to the CURRENT config file)
244
            if (result.rootDir) {
70✔
245
                result.rootDir = path.resolve(projectFileCwd, result.rootDir);
11✔
246
            }
247
            if (result.outDir) {
70✔
248
                result.outDir = path.resolve(projectFileCwd, result.outDir);
4✔
249
            }
250
            if (result.cwd) {
70✔
251
                result.cwd = path.resolve(projectFileCwd, result.cwd);
1✔
252
            }
253
            if (result.sourceRoot && result.resolveSourceRoot) {
70✔
254
                result.sourceRoot = path.resolve(projectFileCwd, result.sourceRoot);
2✔
255
            }
256
            return result;
70✔
257
        }
258
    }
259

260
    /**
261
     * Convert relative paths to absolute paths, relative to the given directory. Also de-dupes the paths. Modifies the array in-place
262
     * @param collection usually a bsconfig.
263
     * @param key a key of the config to read paths from (usually this is `'plugins'` or `'require'`)
264
     * @param relativeDir the path to the folder where the paths should be resolved relative to. This should be an absolute path
265
     */
266
    public resolvePathsRelativeTo(collection: any, key: string, relativeDir: string) {
267
        if (!collection[key]) {
146✔
268
            return;
142✔
269
        }
270
        const result = new Set<string>();
4✔
271
        for (const p of collection[key] as string[] ?? []) {
4!
272
            if (p) {
12✔
273
                result.add(
11✔
274
                    p?.startsWith('.') ? path.resolve(relativeDir, p) : p
44!
275
                );
276
            }
277
        }
278
        collection[key] = [...result];
4✔
279
    }
280

281
    /**
282
     * Given a BsConfig object, start with defaults,
283
     * merge with bsconfig.json and the provided options.
284
     * @param config a bsconfig object to use as the baseline for the resulting config
285
     */
286
    public normalizeAndResolveConfig(config: BsConfig | undefined): FinalizedBsConfig {
287
        let result = this.normalizeConfig({});
155✔
288

289
        if (config?.noProject) {
155✔
290
            return result;
1✔
291
        }
292

293
        //if no options were provided, try to find a bsconfig.json file
294
        if (!config || !config.project) {
154✔
295
            result.project = this.getConfigFilePath(config?.cwd);
96✔
296
        } else {
297
            //use the config's project link
298
            result.project = config.project;
58✔
299
        }
300
        if (result.project) {
154✔
301
            let configFile = this.loadConfigFile(result.project, undefined, config?.cwd);
61!
302
            result = Object.assign(result, configFile);
58✔
303
        }
304
        //override the defaults with the specified options
305
        result = Object.assign(result, config);
151✔
306
        return result;
151✔
307
    }
308

309
    /**
310
     * Set defaults for any missing items
311
     * @param config a bsconfig object to use as the baseline for the resulting config
312
     */
313
    public normalizeConfig(config: BsConfig | undefined): FinalizedBsConfig {
314
        config = config ?? {} as BsConfig;
2,447✔
315

316
        const cwd = config.cwd ?? process.cwd();
2,447✔
317

318
        let logLevel: LogLevel = LogLevel.log;
2,447✔
319

320
        if (typeof config.logLevel === 'string') {
2,447✔
321
            logLevel = LogLevel[(config.logLevel as string).toLowerCase()] ?? LogLevel.log;
2!
322
        }
323

324
        let bslibDestinationDir = config.bslibDestinationDir ?? 'source';
2,447✔
325
        if (bslibDestinationDir !== 'source') {
2,447✔
326
            // strip leading and trailing slashes
327
            bslibDestinationDir = bslibDestinationDir.replace(/^(\/*)(.*?)(\/*)$/, '$2');
4✔
328
        }
329

330
        let noEmit: boolean;
331
        if ('noEmit' in config) {
2,447✔
332
            noEmit = config.noEmit;
152✔
333
        } else if ('copyToStaging' in config) {
2,295✔
334
            noEmit = !(config as any).copyToStaging; //invert the old value
2✔
335
        } else {
336
            noEmit = false; //default case
2,293✔
337
        }
338

339
        let outDir: string;
340
        if ('outDir' in config) {
2,447✔
341
            outDir = config.outDir ?? './out';
647!
342
        } else if ('stagingFolderPath' in config) {
1,800✔
343
            outDir = (config as any).stagingFolderPath as string;
1✔
344
        } else if ('stagingDir' in config) {
1,799✔
345
            outDir = (config as any).stagingDir as string;
1✔
346
        } else {
347
            outDir = './out'; //default case
1,798✔
348
        }
349

350
        const configWithDefaults: Omit<FinalizedBsConfig, 'rootDir'> = {
2,447✔
351
            cwd: cwd,
352
            //use default files array from rokuDeploy
353
            files: config.files ?? [...DefaultFiles],
7,341✔
354
            outDir: outDir,
355
            sourceMap: config.sourceMap === true,
356
            watch: config.watch === true ? true : false,
2,447!
357
            emitFullPaths: config.emitFullPaths === true ? true : false,
2,447!
358
            noEmit: noEmit,
359
            ignoreErrorCodes: config.ignoreErrorCodes ?? [],
7,341✔
360
            diagnosticSeverityOverrides: config.diagnosticSeverityOverrides ?? {},
7,341✔
361
            diagnosticFilters: config.diagnosticFilters ?? [],
7,341✔
362
            diagnosticFiltersV0Compatibility: config.diagnosticFiltersV0Compatibility === true ? true : false,
2,447!
363
            plugins: config.plugins ?? [],
7,341✔
364
            pruneEmptyCodeFiles: config.pruneEmptyCodeFiles === true ? true : false,
2,447✔
365
            autoImportComponentScript: config.autoImportComponentScript === true ? true : false,
2,447✔
366
            showDiagnosticsInConsole: config.showDiagnosticsInConsole === false ? false : true,
2,447✔
367
            sourceRoot: config.sourceRoot ? standardizePath(config.sourceRoot) : undefined,
2,447✔
368
            resolveSourceRoot: config.resolveSourceRoot === true ? true : false,
2,447!
369
            allowBrighterScriptInBrightScript: config.allowBrighterScriptInBrightScript === true ? true : false,
2,447!
370
            emitDefinitions: config.emitDefinitions === true ? true : false,
2,447!
371
            removeParameterTypes: config.removeParameterTypes === true ? true : false,
2,447!
372
            logLevel: logLevel,
373
            bslibDestinationDir: bslibDestinationDir,
374
            legacyCallfuncHandling: config.legacyCallfuncHandling === true ? true : false
2,447!
375
        };
376

377
        //mutate `config` in case anyone is holding a reference to the incomplete one
378
        const merged: FinalizedBsConfig = Object.assign(config, configWithDefaults);
2,447✔
379

380
        return merged;
2,447✔
381
    }
382

383
    /**
384
     * Get the root directory from options.
385
     * Falls back to options.cwd.
386
     * Falls back to process.cwd
387
     * @param options a bsconfig object
388
     */
389
    public getRootDir(options: BsConfig) {
390
        if (!options) {
2,243!
UNCOV
391
            throw new Error('Options is required');
×
392
        }
393
        let cwd = options.cwd;
2,243✔
394
        cwd = cwd ? cwd : process.cwd();
2,243!
395
        let rootDir = options.rootDir ? options.rootDir : cwd;
2,243✔
396

397
        rootDir = path.resolve(cwd, rootDir);
2,243✔
398

399
        return rootDir;
2,243✔
400
    }
401

402
    /**
403
     * Given a list of callables as a dictionary indexed by their full name (namespace included, transpiled to underscore-separated.
404
     */
405
    public getCallableContainersByLowerName(callables: CallableContainer[]): CallableContainerMap {
406
        //find duplicate functions
407
        const result = new Map<string, CallableContainer[]>();
2,026✔
408

409
        for (let callableContainer of callables) {
2,026✔
410
            let lowerName = callableContainer.callable.getName(ParseMode.BrightScript).toLowerCase();
158,244✔
411

412
            //create a new array for this name
413
            const list = result.get(lowerName);
158,244✔
414
            if (list) {
158,244✔
415
                list.push(callableContainer);
6,112✔
416
            } else {
417
                result.set(lowerName, [callableContainer]);
152,132✔
418
            }
419
        }
420
        return result;
2,026✔
421
    }
422

423
    /**
424
     * Given an absolute path to a source file, and a target path,
425
     * compute the pkg path for the target relative to the source file's location
426
     */
427
    public getPkgPathFromTarget(containingFilePathAbsolute: string, targetPath: string) {
428
        // https://regex101.com/r/w7CG2N/1
429
        const regexp = /^(?:pkg|libpkg):(\/)?/i;
704✔
430
        const [fullScheme, slash] = regexp.exec(targetPath) ?? [];
704✔
431
        //if the target starts with 'pkg:' or 'libpkg:' then it's an absolute path. Return as is
432
        if (slash) {
704✔
433
            targetPath = targetPath.substring(fullScheme.length);
433✔
434
            if (targetPath === '') {
433✔
435
                return null;
2✔
436
            } else {
437
                return path.normalize(targetPath);
431✔
438
            }
439
        }
440
        //if the path is exactly `pkg:` or `libpkg:`
441
        if (targetPath === fullScheme && !slash) {
271✔
442
            return null;
2✔
443
        }
444

445
        //remove the filename
446
        let containingFolder = path.normalize(path.dirname(containingFilePathAbsolute));
269✔
447
        //start with the containing folder, split by slash
448
        let result = containingFolder.split(path.sep);
269✔
449

450
        //split on slash
451
        let targetParts = path.normalize(targetPath).split(path.sep);
269✔
452

453
        for (let part of targetParts) {
269✔
454
            if (part === '' || part === '.') {
273✔
455
                //do nothing, it means current directory
456
                continue;
4✔
457
            }
458
            if (part === '..') {
269✔
459
                //go up one directory
460
                result.pop();
2✔
461
            } else {
462
                result.push(part);
267✔
463
            }
464
        }
465
        return result.join(path.sep);
269✔
466
    }
467

468
    /**
469
     * Compute the relative path from the source file to the target file
470
     * @param pkgSrcPath  - the absolute path to the source, where cwd is the package location
471
     * @param pkgTargetPath  - the absolute path to the target, where cwd is the package location
472
     */
473
    public getRelativePath(pkgSrcPath: string, pkgTargetPath: string) {
474
        pkgSrcPath = path.normalize(pkgSrcPath);
11✔
475
        pkgTargetPath = path.normalize(pkgTargetPath);
11✔
476

477
        //break by path separator
478
        let sourceParts = pkgSrcPath.split(path.sep);
11✔
479
        let targetParts = pkgTargetPath.split(path.sep);
11✔
480

481
        let commonParts = [] as string[];
11✔
482
        //find their common root
483
        for (let i = 0; i < targetParts.length; i++) {
11✔
484
            if (targetParts[i].toLowerCase() === sourceParts[i].toLowerCase()) {
17✔
485
                commonParts.push(targetParts[i]);
6✔
486
            } else {
487
                //we found a non-matching part...so no more commonalities past this point
488
                break;
11✔
489
            }
490
        }
491

492
        //throw out the common parts from both sets
493
        sourceParts.splice(0, commonParts.length);
11✔
494
        targetParts.splice(0, commonParts.length);
11✔
495

496
        //throw out the filename part of source
497
        sourceParts.splice(sourceParts.length - 1, 1);
11✔
498
        //start out by adding updir paths for each remaining source part
499
        let resultParts = sourceParts.map(() => '..');
11✔
500

501
        //now add every target part
502
        resultParts = [...resultParts, ...targetParts];
11✔
503
        return path.join(...resultParts);
11✔
504
    }
505

506
    public getImportPackagePath(srcPath: string, pkgTargetPath: string) {
507
        const srcExt = this.getExtension(srcPath);
6✔
508
        const lowerSrcExt = srcExt.toLowerCase();
6✔
509
        const lowerTargetExt = this.getExtension(pkgTargetPath).toLowerCase();
6✔
510
        if (lowerSrcExt === '.bs' && lowerTargetExt === '.brs') {
6✔
511
            // if source is .bs, use that as the import extenstion
512
            return pkgTargetPath.substring(0, pkgTargetPath.length - lowerTargetExt.length) + srcExt;
3✔
513
        }
514
        return pkgTargetPath;
3✔
515
    }
516

517
    /**
518
     * Walks left in a DottedGetExpression and returns a VariableExpression if found, or undefined if not found
519
     */
520
    public findBeginningVariableExpression(dottedGet: DottedGetExpression): VariableExpression | undefined {
521
        let left: any = dottedGet;
53✔
522
        while (left) {
53✔
523
            if (isVariableExpression(left)) {
85✔
524
                return left;
53✔
525
            } else if (isDottedGetExpression(left)) {
32!
526
                left = left.obj;
32✔
527
            } else {
UNCOV
528
                break;
×
529
            }
530
        }
531
    }
532

533
    /**
534
     * Do `a` and `b` overlap by at least one character. This returns false if they are at the edges. Here's some examples:
535
     * ```
536
     * | true | true | true | true | true | false | false | false | false |
537
     * |------|------|------|------|------|-------|-------|-------|-------|
538
     * | aa   |  aaa |  aaa | aaa  |  a   |  aa   |    aa | a     |     a |
539
     * |  bbb | bb   |  bbb |  b   | bbb  |    bb |  bb   |     b | a     |
540
     * ```
541
     */
542
    public rangesIntersect(a: Range | undefined, b: Range | undefined) {
543
        //stop if the either range is misisng
544
        if (!a || !b) {
11✔
545
            return false;
2✔
546
        }
547

548
        // Check if `a` is before `b`
549
        if (a.end.line < b.start.line || (a.end.line === b.start.line && a.end.character <= b.start.character)) {
9✔
550
            return false;
1✔
551
        }
552

553
        // Check if `b` is before `a`
554
        if (b.end.line < a.start.line || (b.end.line === a.start.line && b.end.character <= a.start.character)) {
8✔
555
            return false;
1✔
556
        }
557

558
        // These ranges must intersect
559
        return true;
7✔
560
    }
561

562
    /**
563
     * Do `a` and `b` overlap by at least one character or touch at the edges
564
     * ```
565
     * | true | true | true | true | true | true  | true  | false | false |
566
     * |------|------|------|------|------|-------|-------|-------|-------|
567
     * | aa   |  aaa |  aaa | aaa  |  a   |  aa   |    aa | a     |     a |
568
     * |  bbb | bb   |  bbb |  b   | bbb  |    bb |  bb   |     b | a     |
569
     * ```
570
     */
571
    public rangesIntersectOrTouch(a: Range | undefined, b: Range | undefined) {
572
        //stop if the either range is misisng
573
        if (!a || !b) {
35✔
574
            return false;
2✔
575
        }
576
        // Check if `a` is before `b`
577
        if (a.end.line < b.start.line || (a.end.line === b.start.line && a.end.character < b.start.character)) {
33✔
578
            return false;
3✔
579
        }
580

581
        // Check if `b` is before `a`
582
        if (b.end.line < a.start.line || (b.end.line === a.start.line && b.end.character < a.start.character)) {
30✔
583
            return false;
2✔
584
        }
585

586
        // These ranges must intersect
587
        return true;
28✔
588
    }
589

590
    /**
591
     * Test if `position` is in `range`. If the position is at the edges, will return true.
592
     * Adapted from core vscode
593
     */
594
    public rangeContains(range: Range | undefined, position: Position | undefined) {
595
        return this.comparePositionToRange(position, range) === 0;
13,405✔
596
    }
597

598
    public comparePositionToRange(position: Position | undefined, range: Range | undefined) {
599
        //stop if the either range is missng
600
        if (!position || !range) {
15,566✔
601
            return 0;
3✔
602
        }
603

604
        if (this.comparePosition(position, range.start) < 0) {
15,563✔
605
            return -1;
2,479✔
606
        }
607
        if (this.comparePosition(position, range.end) > 0) {
13,084✔
608
            return 1;
9,498✔
609
        }
610
        return 0;
3,586✔
611
    }
612

613
    public comparePosition(a: Position | undefined, b: Position) {
614
        //stop if the either position is missing
615
        if (!a || !b) {
249,862!
UNCOV
616
            return 0;
×
617
        }
618

619
        if (a.line < b.line || (a.line === b.line && a.character < b.character)) {
249,862✔
620
            return -1;
17,533✔
621
        }
622
        if (a.line > b.line || (a.line === b.line && a.character > b.character)) {
232,329✔
623
            return 1;
231,274✔
624
        }
625
        return 0;
1,055✔
626
    }
627

628
    /**
629
     * Is the inner range completely enclosed in the outer range
630
     */
631
    public isRangeInRange(inner: Range, outer: Range) {
632
        return this.comparePosition(inner.start, outer.start) === 1 &&
5✔
633
            this.comparePosition(inner.end, outer.end) === -1;
634
    }
635

636
    /**
637
     * Combine all the documentation for a node - uses the AstNode's leadingTrivia property
638
     * @param node the node to get the documentation for
639
     * @param options extra options
640
     * @param options.prettyPrint if true, will format the comment text for markdown
641
     * @param options.commentTokens out Array of tokens that match the comment lines
642
     */
643
    public getNodeDocumentation(node: AstNode, options: { prettyPrint?: boolean; commentTokens?: Token[] } = { prettyPrint: true }) {
1,070✔
644
        if (!node) {
37,761✔
645
            return '';
1,361✔
646
        }
647
        options = options ?? { prettyPrint: true };
36,400!
648
        options.commentTokens = options.commentTokens ?? [];
36,400✔
649
        const nodeTrivia = node.leadingTrivia ?? [];
36,400✔
650
        const leadingTrivia = isStatement(node)
36,400✔
651
            ? [...(node.annotations?.map(anno => anno.leadingTrivia ?? []).flat() ?? []), ...nodeTrivia]
32!
652
            : nodeTrivia;
653
        const tokens = leadingTrivia?.filter(t => t.kind === TokenKind.Newline || t.kind === TokenKind.Comment);
70,370!
654
        const comments = [] as Token[];
36,400✔
655

656
        let newLinesInRow = 0;
36,400✔
657
        for (let i = tokens.length - 1; i >= 0; i--) {
36,400✔
658
            const token = tokens[i];
32,310✔
659
            //skip whitespace and newline chars
660
            if (token.kind === TokenKind.Comment) {
32,310✔
661
                comments.push(token);
1,154✔
662
                newLinesInRow = 0;
1,154✔
663
            } else if (token.kind === TokenKind.Newline) {
31,156!
664
                //skip these tokens
665
                newLinesInRow++;
31,156✔
666

667
                if (newLinesInRow > 1) {
31,156✔
668
                    // stop processing on empty line.
669
                    break;
3,759✔
670
                }
671
                //any other token means there are no more comments
672
            } else {
UNCOV
673
                break;
×
674
            }
675
        }
676
        const jsDocCommentBlockLine = /(\/\*{2,}|\*{1,}\/)/i;
36,400✔
677
        let usesjsDocCommentBlock = false;
36,400✔
678
        if (comments.length === 0) {
36,400✔
679
            return '';
35,409✔
680
        }
681
        return comments.reverse()
991✔
682
            .map(x => ({ line: x.text.replace(/^('|rem)/i, '').trim(), token: x }))
1,154✔
683
            .filter(({ line }) => {
684
                if (jsDocCommentBlockLine.exec(line)) {
1,154✔
685
                    usesjsDocCommentBlock = true;
46✔
686
                    return false;
46✔
687
                }
688
                return true;
1,108✔
689
            }).map(({ line, token }) => {
690
                if (usesjsDocCommentBlock) {
1,108✔
691
                    if (line.startsWith('*')) {
39✔
692
                        //remove jsDoc leading '*'
693
                        line = line.slice(1).trim();
38✔
694
                    }
695
                }
696
                if (options.prettyPrint && line.startsWith('@')) {
1,108✔
697
                    // Handle jsdoc/brightscriptdoc tags specially
698
                    // make sure they are on their own markdown line, and add italics
699
                    const firstSpaceIndex = line.indexOf(' ');
3✔
700
                    if (firstSpaceIndex === -1) {
3✔
701
                        return `\n_${line}_`;
1✔
702
                    }
703
                    const firstWord = line.substring(0, firstSpaceIndex);
2✔
704
                    return `\n_${firstWord}_ ${line.substring(firstSpaceIndex + 1)}`;
2✔
705
                }
706
                if (options.commentTokens) {
1,105!
707
                    options.commentTokens.push(token);
1,105✔
708
                }
709
                return line;
1,105✔
710
            }).join('\n');
711
    }
712

713
    /**
714
     * Prefixes a component name so it can be used as type in the symbol table, without polluting available symbols
715
     *
716
     * @param sgNodeName the Name of the component
717
     * @returns the node name, prefixed with `roSGNode`
718
     */
719
    public getSgNodeTypeName(sgNodeName: string) {
720
        return 'roSGNode' + sgNodeName;
398,384✔
721
    }
722

723
    /**
724
     * Force the drive letter to lower case
725
     */
726
    public driveLetterToLower(fullPath: string) {
727
        if (fullPath) {
2!
728
            let firstCharCode = fullPath.charCodeAt(0);
2✔
729
            if (
2!
730
                //is upper case A-Z
731
                firstCharCode >= 65 && firstCharCode <= 90 &&
6✔
732
                //next char is colon
733
                fullPath[1] === ':'
734
            ) {
735
                fullPath = fullPath[0].toLowerCase() + fullPath.substring(1);
2✔
736
            }
737
        }
738
        return fullPath;
2✔
739
    }
740

741
    /**
742
     * Replace the first instance of `search` in `subject` with `replacement`
743
     */
744
    public replaceCaseInsensitive(subject: string, search: string, replacement: string) {
745
        let idx = subject.toLowerCase().indexOf(search.toLowerCase());
8,207✔
746
        if (idx > -1) {
8,207✔
747
            let result = subject.substring(0, idx) + replacement + subject.substring(idx + search.length);
2,538✔
748
            return result;
2,538✔
749
        } else {
750
            return subject;
5,669✔
751
        }
752
    }
753

754
    /**
755
     * Does the string appear to be a uri (i.e. does it start with `file:`)
756
     */
757
    public isUriLike(filePath: string) {
758
        return filePath?.indexOf('file:') === 0;// eslint-disable-line @typescript-eslint/prefer-string-starts-ends-with
325,895!
759
    }
760

761
    /**
762
     * Given a file path, convert it to a URI string
763
     */
764
    public pathToUri(filePath: string) {
765
        if (!filePath) {
294,516✔
766
            return filePath;
33,631✔
767
        } else if (this.isUriLike(filePath)) {
260,885✔
768
            return filePath;
234,951✔
769
        } else {
770
            return URI.file(filePath).toString();
25,934✔
771
        }
772
    }
773

774
    /**
775
     * Given a URI, convert that to a regular fs path
776
     */
777
    public uriToPath(uri: string) {
778
        //if this doesn't look like a URI, then assume it's already a path
779
        if (this.isUriLike(uri) === false) {
60,943✔
780
            return uri;
27✔
781
        }
782
        let parsedPath = URI.parse(uri).fsPath;
60,916✔
783

784
        //Uri annoyingly converts all drive letters to lower case...so this will bring back whatever case it came in as
785
        let match = /\/\/\/([a-z]:)/i.exec(uri);
60,916✔
786
        if (match) {
60,916✔
787
            let originalDriveCasing = match[1];
2✔
788
            parsedPath = originalDriveCasing + parsedPath.substring(2);
2✔
789
        }
790
        const normalizedPath = path.normalize(parsedPath);
60,916✔
791
        return normalizedPath;
60,916✔
792
    }
793

794
    /**
795
     * Get paths to all files on disc that match this project's source list
796
     */
797
    public async getFilePaths(options: FinalizedBsConfig) {
798
        let rootDir = this.getRootDir(options);
125✔
799

800
        let files = await rokuDeploy.getFilePaths(options.files, rootDir);
125✔
801
        return files;
125✔
802
    }
803

804
    /**
805
     * Given a path to a brs file, compute the path to a theoretical d.bs file.
806
     * Only `.brs` files can have typedef path, so return undefined for everything else
807
     */
808
    public getTypedefPath(brsSrcPath: string) {
809
        const typedefPath = brsSrcPath
4,069✔
810
            .replace(/\.brs$/i, '.d.bs')
811
            .toLowerCase();
812

813
        if (typedefPath.endsWith('.d.bs')) {
4,069✔
814
            return typedefPath;
2,304✔
815
        } else {
816
            return undefined;
1,765✔
817
        }
818
    }
819

820
    /**
821
     * Set a timeout for the specified milliseconds, and resolve the promise once the timeout is finished.
822
     * @param milliseconds the minimum number of milliseconds to sleep for
823
     */
824
    public sleep(milliseconds: number) {
825
        return new Promise((resolve) => {
1,129✔
826
            //if milliseconds is 0, don't actually timeout (improves unit test throughput)
827
            if (milliseconds === 0) {
1,129✔
828
                process.nextTick(resolve);
1,011✔
829
            } else {
830
                setTimeout(resolve, milliseconds);
118✔
831
            }
832
        });
833
    }
834

835
    /**
836
     * Determines if the position is greater than the range. This means
837
     * the position does not touch the range, and has a position greater than the end
838
     * of the range. A position that touches the last line/char of a range is considered greater
839
     * than the range, because the `range.end` is EXclusive
840
     */
841
    public positionIsGreaterThanRange(position: Position, range: Range) {
842

843
        //if the position is a higher line than the range
844
        if (position.line > range.end.line) {
1,301✔
845
            return true;
1,212✔
846
        } else if (position.line < range.end.line) {
89!
UNCOV
847
            return false;
×
848
        }
849
        //they are on the same line
850

851
        //if the position's char is greater than or equal to the range's
852
        if (position.character >= range.end.character) {
89!
853
            return true;
89✔
854
        } else {
UNCOV
855
            return false;
×
856
        }
857
    }
858

859
    /**
860
     * Get a range back from an object that contains (or is) a range
861
     */
862
    public extractRange(rangeIsh: RangeLike): Range | undefined {
863
        if (!rangeIsh) {
10,985✔
864
            return undefined;
29✔
865
        } else if ('location' in rangeIsh) {
10,956✔
866
            return rangeIsh.location?.range;
7,613✔
867
        } else if ('range' in rangeIsh) {
3,343!
868
            return rangeIsh.range;
3,343✔
UNCOV
869
        } else if (Range.is(rangeIsh)) {
×
870
            return rangeIsh;
×
871
        } else {
UNCOV
872
            return undefined;
×
873
        }
874
    }
875

876
    /**
877
     * If the two items have lines that touch
878
     */
879
    public linesTouch(first: RangeLike, second: RangeLike) {
880
        const firstRange = this.extractRange(first);
1,700✔
881
        const secondRange = this.extractRange(second);
1,700✔
882
        if (firstRange && secondRange && (
1,700✔
883
            firstRange.start.line === secondRange.start.line ||
884
            firstRange.start.line === secondRange.end.line ||
885
            firstRange.end.line === secondRange.start.line ||
886
            firstRange.end.line === secondRange.end.line
887
        )) {
888
            return true;
91✔
889
        } else {
890
            return false;
1,609✔
891
        }
892
    }
893

894
    /**
895
     * Find a script import that the current position touches, or undefined if not found
896
     */
897
    public getScriptImportAtPosition(scriptImports: FileReference[], position: Position): FileReference | undefined {
898
        let scriptImport = scriptImports.find((x) => {
128✔
899
            return x.filePathRange &&
5✔
900
                x.filePathRange.start.line === position.line &&
901
                //column between start and end
902
                position.character >= x.filePathRange.start.character &&
903
                position.character <= x.filePathRange.end.character;
904
        });
905
        return scriptImport;
128✔
906
    }
907

908
    /**
909
     * Given the class name text, return a namespace-prefixed name.
910
     * If the name already has a period in it, or the namespaceName was not provided, return the class name as is.
911
     * If the name does not have a period, and a namespaceName was provided, return the class name prepended by the namespace name.
912
     * If no namespace is provided, return the `className` unchanged.
913
     */
914
    public getFullyQualifiedClassName(className: string, namespaceName?: string) {
915
        if (className?.includes('.') === false && namespaceName) {
4,745✔
916
            return `${namespaceName}.${className}`;
260✔
917
        } else {
918
            return className;
4,485✔
919
        }
920
    }
921

922
    public splitIntoLines(string: string) {
923
        return string.split(/\r?\n/g);
169✔
924
    }
925

926
    public getTextForRange(string: string | string[], range: Range): string {
927
        let lines: string[];
928
        if (Array.isArray(string)) {
171✔
929
            lines = string;
170✔
930
        } else {
931
            lines = this.splitIntoLines(string);
1✔
932
        }
933

934
        const start = range.start;
171✔
935
        const end = range.end;
171✔
936

937
        let endCharacter = end.character;
171✔
938
        // If lines are the same we need to subtract out our new starting position to make it work correctly
939
        if (start.line === end.line) {
171✔
940
            endCharacter -= start.character;
159✔
941
        }
942

943
        let rangeLines = [lines[start.line].substring(start.character)];
171✔
944
        for (let i = start.line + 1; i <= end.line; i++) {
171✔
945
            rangeLines.push(lines[i]);
12✔
946
        }
947
        const lastLine = rangeLines.pop();
171✔
948
        if (lastLine !== undefined) {
171!
949
            rangeLines.push(lastLine.substring(0, endCharacter));
171✔
950
        }
951
        return rangeLines.join('\n');
171✔
952
    }
953

954
    /**
955
     * Helper for creating `Location` objects. Prefer using this function because vscode-languageserver's `Location.create()` is significantly slower at scale
956
     */
957
    public createLocationFromRange(uri: string, range: Range): Location {
958
        return {
9,823✔
959
            uri: util.pathToUri(uri),
960
            range: range
961
        };
962
    }
963

964
    /**
965
     * Helper for creating `Location` objects from a file and range
966
     */
967
    public createLocationFromFileRange(file: BscFile, range: Range): Location {
968
        return this.createLocationFromRange(this.pathToUri(file?.srcPath), range);
431!
969
    }
970

971
    /**
972
     * 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
973
     */
974
    public createLocation(startLine: number, startCharacter: number, endLine: number, endCharacter: number, uri?: string): Location {
975
        return {
271,650✔
976
            uri: util.pathToUri(uri),
977
            range: {
978
                start: {
979
                    line: startLine,
980
                    character: startCharacter
981
                },
982
                end: {
983
                    line: endLine,
984
                    character: endCharacter
985
                }
986
            }
987
        };
988
    }
989

990
    /**
991
     * Helper for creating `Range` objects. Prefer using this function because vscode-languageserver's `Range.create()` is significantly slower.
992
     */
993
    public createRange(startLine: number, startCharacter: number, endLine: number, endCharacter: number): Range {
994
        return {
9,895✔
995
            start: {
996
                line: startLine,
997
                character: startCharacter
998
            },
999
            end: {
1000
                line: endLine,
1001
                character: endCharacter
1002
            }
1003
        };
1004
    }
1005

1006
    /**
1007
     * Create a `Range` from two `Position`s
1008
     */
1009
    public createRangeFromPositions(startPosition: Position, endPosition: Position): Range | undefined {
1010
        startPosition = startPosition ?? endPosition;
171✔
1011
        endPosition = endPosition ?? startPosition;
171✔
1012
        if (!startPosition && !endPosition) {
171!
UNCOV
1013
            return undefined;
×
1014
        }
1015
        return this.createRange(startPosition.line, startPosition.character, endPosition.line, endPosition.character);
171✔
1016
    }
1017

1018
    /**
1019
     * Clone a range
1020
     */
1021
    public cloneLocation(location: Location) {
1022
        if (location) {
17,390✔
1023
            return {
17,175✔
1024
                uri: location.uri,
1025
                range: {
1026
                    start: {
1027
                        line: location.range.start.line,
1028
                        character: location.range.start.character
1029
                    },
1030
                    end: {
1031
                        line: location.range.end.line,
1032
                        character: location.range.end.character
1033
                    }
1034
                }
1035
            };
1036
        } else {
1037
            return location;
215✔
1038
        }
1039
    }
1040

1041
    /**
1042
     * Clone every token
1043
     */
1044
    public cloneToken<T extends Token>(token: T): T {
1045
        if (token) {
2,868✔
1046
            const result = {
2,661✔
1047
                kind: token.kind,
1048
                location: this.cloneLocation(token.location),
1049
                text: token.text,
1050
                isReserved: token.isReserved,
1051
                leadingWhitespace: token.leadingWhitespace,
1052
                leadingTrivia: token.leadingTrivia.map(x => this.cloneToken(x))
1,356✔
1053
            } as Token;
1054
            //handle those tokens that have charCode
1055
            if ('charCode' in token) {
2,661✔
1056
                (result as any).charCode = (token as any).charCode;
3✔
1057
            }
1058
            return result as T;
2,661✔
1059
        } else {
1060
            return token;
207✔
1061
        }
1062
    }
1063

1064
    /**
1065
     *  Gets the bounding range of a bunch of ranges or objects that have ranges
1066
     *  TODO: this does a full iteration of the args. If the args were guaranteed to be in range order, we could optimize this
1067
     */
1068
    public createBoundingLocation(...locatables: Array<{ location?: Location } | Location | { range?: Range } | Range | undefined>): Location | undefined {
1069
        let uri: string | undefined;
1070
        let startPosition: Position | undefined;
1071
        let endPosition: Position | undefined;
1072

1073
        for (let locatable of locatables) {
52,667✔
1074
            let range: Range;
1075
            if (!locatable) {
214,198✔
1076
                continue;
57,209✔
1077
            } else if ('location' in locatable) {
156,989✔
1078
                range = locatable.location?.range;
150,963✔
1079
                if (!uri) {
150,963✔
1080
                    uri = locatable.location?.uri;
59,624✔
1081
                }
1082
            } else if (Location.is(locatable)) {
6,026✔
1083
                range = locatable.range;
6,014✔
1084
                if (!uri) {
6,014✔
1085
                    uri = locatable.uri;
5,378✔
1086
                }
1087
            } else if ('range' in locatable) {
12!
UNCOV
1088
                range = locatable.range;
×
1089
            } else {
1090
                range = locatable as Range;
12✔
1091
            }
1092

1093
            //skip undefined locations or locations without a range
1094
            if (!range) {
156,989✔
1095
                continue;
3,751✔
1096
            }
1097

1098
            if (!startPosition) {
153,238✔
1099
                startPosition = range.start;
51,406✔
1100
            } else if (this.comparePosition(range.start, startPosition) < 0) {
101,832✔
1101
                startPosition = range.start;
979✔
1102
            }
1103
            if (!endPosition) {
153,238✔
1104
                endPosition = range.end;
51,406✔
1105
            } else if (this.comparePosition(range.end, endPosition) > 0) {
101,832✔
1106
                endPosition = range.end;
95,700✔
1107
            }
1108
        }
1109
        if (startPosition && endPosition) {
52,667✔
1110
            return util.createLocation(startPosition.line, startPosition.character, endPosition.line, endPosition.character, uri);
51,406✔
1111
        } else {
1112
            return undefined;
1,261✔
1113
        }
1114
    }
1115

1116
    /**
1117
     *  Gets the bounding range of a bunch of ranges or objects that have ranges
1118
     *  TODO: this does a full iteration of the args. If the args were guaranteed to be in range order, we could optimize this
1119
     */
1120
    public createBoundingRange(...locatables: Array<RangeLike>): Range | undefined {
1121
        return this.createBoundingLocation(...locatables)?.range;
572✔
1122
    }
1123

1124
    /**
1125
     * Gets the bounding range of an object that contains a bunch of tokens
1126
     * @param tokens Object with tokens in it
1127
     * @returns Range containing all the tokens
1128
     */
1129
    public createBoundingLocationFromTokens(tokens: Record<string, { location?: Location }>): Location | undefined {
1130
        let uri: string;
1131
        let startPosition: Position | undefined;
1132
        let endPosition: Position | undefined;
1133
        for (let key in tokens) {
5,825✔
1134
            let token = tokens?.[key];
20,711!
1135
            let locatableRange = token?.location?.range;
20,711✔
1136
            if (!locatableRange) {
20,711✔
1137
                continue;
6,281✔
1138
            }
1139

1140
            if (!startPosition) {
14,430✔
1141
                startPosition = locatableRange.start;
5,663✔
1142
            } else if (this.comparePosition(locatableRange.start, startPosition) < 0) {
8,767✔
1143
                startPosition = locatableRange.start;
2,401✔
1144
            }
1145
            if (!endPosition) {
14,430✔
1146
                endPosition = locatableRange.end;
5,663✔
1147
            } else if (this.comparePosition(locatableRange.end, endPosition) > 0) {
8,767✔
1148
                endPosition = locatableRange.end;
6,156✔
1149
            }
1150
            if (!uri) {
14,430✔
1151
                uri = token.location.uri;
6,846✔
1152
            }
1153
        }
1154
        if (startPosition && endPosition) {
5,825✔
1155
            return this.createLocation(startPosition.line, startPosition.character, endPosition.line, endPosition.character, uri);
5,663✔
1156
        } else {
1157
            return undefined;
162✔
1158
        }
1159
    }
1160

1161
    /**
1162
     * Create a `Position` object. Prefer this over `Position.create` for performance reasons.
1163
     */
1164
    public createPosition(line: number, character: number) {
1165
        return {
405✔
1166
            line: line,
1167
            character: character
1168
        };
1169
    }
1170

1171
    /**
1172
     * Convert a token into a BscType
1173
     */
1174
    public tokenToBscType(token: Token) {
1175
        // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
1176
        switch (token.kind) {
2,269,134✔
1177
            case TokenKind.Boolean:
2,272,770✔
1178
                return BooleanType.instance;
332✔
1179
            case TokenKind.True:
1180
            case TokenKind.False:
1181
                return BooleanType.instance;
223✔
1182
            case TokenKind.Double:
1183
                return DoubleType.instance;
102✔
1184
            case TokenKind.DoubleLiteral:
1185
                return DoubleType.instance;
9✔
1186
            case TokenKind.Dynamic:
1187
                return DynamicType.instance;
321✔
1188
            case TokenKind.Float:
1189
                return FloatType.instance;
499✔
1190
            case TokenKind.FloatLiteral:
1191
                return FloatType.instance;
131✔
1192
            case TokenKind.Function:
1193
                return FunctionType.instance;
186✔
1194
            case TokenKind.Integer:
1195
                return IntegerType.instance;
1,733✔
1196
            case TokenKind.IntegerLiteral:
1197
                return IntegerType.instance;
2,049✔
1198
            case TokenKind.Invalid:
1199
                return InvalidType.instance;
127✔
1200
            case TokenKind.LongInteger:
1201
                return LongIntegerType.instance;
102✔
1202
            case TokenKind.LongIntegerLiteral:
1203
                return LongIntegerType.instance;
10✔
1204
            case TokenKind.Object:
1205
                return ObjectType.instance;
516✔
1206
            case TokenKind.String:
1207
                return StringType.instance;
3,057✔
1208
            case TokenKind.StringLiteral:
1209
            case TokenKind.TemplateStringExpressionBegin:
1210
            case TokenKind.TemplateStringExpressionEnd:
1211
            case TokenKind.TemplateStringQuasi:
1212
                return StringType.instance;
1,305✔
1213
            case TokenKind.Void:
1214
                return VoidType.instance;
130✔
1215
            case TokenKind.Identifier:
1216
                switch (token.text.toLowerCase()) {
2,257,845✔
1217
                    case 'boolean':
1,234,860!
1218
                        return BooleanType.instance;
288,059✔
1219
                    case 'double':
1220
                        return DoubleType.instance;
2,123✔
1221
                    case 'dynamic':
UNCOV
1222
                        return DynamicType.instance;
×
1223
                    case 'float':
1224
                        return FloatType.instance;
268,995✔
1225
                    case 'function':
UNCOV
1226
                        return FunctionType.instance;
×
1227
                    case 'integer':
1228
                        return IntegerType.instance;
228,757✔
1229
                    case 'invalid':
UNCOV
1230
                        return InvalidType.instance;
×
1231
                    case 'longinteger':
1232
                        return LongIntegerType.instance;
5✔
1233
                    case 'object':
UNCOV
1234
                        return ObjectType.instance;
×
1235
                    case 'string':
1236
                        return StringType.instance;
446,916✔
1237
                    case 'void':
1238
                        return VoidType.instance;
5✔
1239
                }
1240
        }
1241
    }
1242

1243
    /**
1244
     * Deciphers the correct types for fields based on docs
1245
     * https://developer.roku.com/en-ca/docs/references/scenegraph/xml-elements/interface.md
1246
     * @param typeDescriptor the type descriptor from the docs
1247
     * @returns {BscType} the known type, or dynamic
1248
     */
1249
    public getNodeFieldType(typeDescriptor: string, lookupTable?: SymbolTable): BscType {
1250
        let typeDescriptorLower = typeDescriptor.toLowerCase().trim().replace(/\*/g, '');
2,236,666✔
1251

1252
        if (typeDescriptorLower.startsWith('as ')) {
2,236,666✔
1253
            typeDescriptorLower = typeDescriptorLower.substring(3).trim();
8,472✔
1254
        }
1255
        const nodeFilter = (new RegExp(/^\[?(.* node)/, 'i')).exec(typeDescriptorLower);
2,236,666✔
1256
        if (nodeFilter?.[1]) {
2,236,666✔
1257
            typeDescriptorLower = nodeFilter[1].trim();
46,596✔
1258
        }
1259
        const parensFilter = (new RegExp(/(.*)\(.*\)/, 'gi')).exec(typeDescriptorLower);
2,236,666✔
1260
        if (parensFilter?.[1]) {
2,236,666✔
1261
            typeDescriptorLower = parensFilter[1].trim();
4,236✔
1262
        }
1263

1264
        const bscType = this.tokenToBscType(createToken(TokenKind.Identifier, typeDescriptorLower));
2,236,666✔
1265
        if (bscType) {
2,236,666✔
1266
            return bscType;
1,234,825✔
1267
        }
1268

1269
        function getRect2dType() {
1270
            const rect2dType = new AssociativeArrayType();
6,358✔
1271
            rect2dType.addMember('height', {}, FloatType.instance, SymbolTypeFlag.runtime);
6,358✔
1272
            rect2dType.addMember('width', {}, FloatType.instance, SymbolTypeFlag.runtime);
6,358✔
1273
            rect2dType.addMember('x', {}, FloatType.instance, SymbolTypeFlag.runtime);
6,358✔
1274
            rect2dType.addMember('y', {}, FloatType.instance, SymbolTypeFlag.runtime);
6,358✔
1275
            return rect2dType;
6,358✔
1276
        }
1277

1278
        function getColorType() {
1279
            return unionTypeFactory([IntegerType.instance, StringType.instance]);
139,792✔
1280
        }
1281

1282
        //check for uniontypes
1283
        const multipleTypes = typeDescriptorLower.split(' or ').map(s => s.trim());
1,006,077✔
1284
        if (multipleTypes.length > 1) {
1,001,841✔
1285
            const individualTypes = multipleTypes.map(t => this.getNodeFieldType(t, lookupTable));
8,472✔
1286
            return unionTypeFactory(individualTypes);
4,236✔
1287
        }
1288

1289
        const typeIsArray = typeDescriptorLower.startsWith('array of ') || typeDescriptorLower.startsWith('roarray of ');
997,605✔
1290

1291
        if (typeIsArray) {
997,605✔
1292
            const ofSearch = ' of ';
122,844✔
1293
            const arrayPrefixLength = typeDescriptorLower.indexOf(ofSearch) + ofSearch.length;
122,844✔
1294
            let arrayOfTypeName = typeDescriptorLower.substring(arrayPrefixLength); //cut off beginnin, eg. 'array of' or 'roarray of'
122,844✔
1295
            if (arrayOfTypeName.endsWith('s')) {
122,844✔
1296
                // remove "s" in "floats", etc.
1297
                arrayOfTypeName = arrayOfTypeName.substring(0, arrayOfTypeName.length - 1);
88,956✔
1298
            }
1299
            if (arrayOfTypeName.endsWith('\'')) {
122,844!
1300
                // remove "'" in "float's", etc.
UNCOV
1301
                arrayOfTypeName = arrayOfTypeName.substring(0, arrayOfTypeName.length - 1);
×
1302
            }
1303
            if (arrayOfTypeName === 'rectangle') {
122,844✔
1304
                arrayOfTypeName = 'rect2d';
2,118✔
1305
            }
1306
            let arrayType = this.getNodeFieldType(arrayOfTypeName, lookupTable);
122,844✔
1307
            return new ArrayType(arrayType);
122,844✔
1308
        } else if (typeDescriptorLower.startsWith('option ')) {
874,761✔
1309
            const actualTypeName = typeDescriptorLower.substring('option '.length); //cut off beginning 'option '
42,360✔
1310
            return this.getNodeFieldType(actualTypeName, lookupTable);
42,360✔
1311
        } else if (typeDescriptorLower.startsWith('value ')) {
832,401✔
1312
            const actualTypeName = typeDescriptorLower.substring('value '.length); //cut off beginning 'value '
16,944✔
1313
            return this.getNodeFieldType(actualTypeName, lookupTable);
16,944✔
1314
        } else if (typeDescriptorLower === 'n/a') {
815,457✔
1315
            return DynamicType.instance;
4,236✔
1316
        } else if (typeDescriptorLower === 'uri') {
811,221✔
1317
            return StringType.instance;
154,619✔
1318
        } else if (typeDescriptorLower === 'color') {
656,602✔
1319
            return getColorType();
139,791✔
1320
        } else if (typeDescriptorLower === 'vector2d' || typeDescriptorLower === 'floatarray') {
516,811✔
1321
            return new ArrayType(FloatType.instance);
50,833✔
1322
        } else if (typeDescriptorLower === 'vector2darray') {
465,978!
UNCOV
1323
            return new ArrayType(new ArrayType(FloatType.instance));
×
1324
        } else if (typeDescriptorLower === 'intarray') {
465,978✔
1325
            return new ArrayType(IntegerType.instance);
1✔
1326
        } else if (typeDescriptorLower === 'colorarray') {
465,977✔
1327
            return new ArrayType(getColorType());
1✔
1328
        } else if (typeDescriptorLower === 'boolarray') {
465,976!
UNCOV
1329
            return new ArrayType(BooleanType.instance);
×
1330
        } else if (typeDescriptorLower === 'stringarray' || typeDescriptorLower === 'strarray') {
465,976✔
1331
            return new ArrayType(StringType.instance);
1✔
1332
        } else if (typeDescriptorLower === 'int') {
465,975✔
1333
            return IntegerType.instance;
12,708✔
1334
        } else if (typeDescriptorLower === 'time') {
453,267✔
1335
            return DoubleType.instance;
40,243✔
1336
        } else if (typeDescriptorLower === 'str') {
413,024!
UNCOV
1337
            return StringType.instance;
×
1338
        } else if (typeDescriptorLower === 'bool') {
413,024✔
1339
            return BooleanType.instance;
2,118✔
1340
        } else if (typeDescriptorLower === 'array' || typeDescriptorLower === 'roarray') {
410,906✔
1341
            return new ArrayType();
16,945✔
1342
        } else if (typeDescriptorLower === 'assocarray' ||
393,961✔
1343
            typeDescriptorLower === 'associative array' ||
1344
            typeDescriptorLower === 'associativearray' ||
1345
            typeDescriptorLower === 'roassociativearray' ||
1346
            typeDescriptorLower.startsWith('associative array of') ||
1347
            typeDescriptorLower.startsWith('associativearray of') ||
1348
            typeDescriptorLower.startsWith('roassociativearray of')
1349
        ) {
1350
            return new AssociativeArrayType();
74,131✔
1351
        } else if (typeDescriptorLower === 'node') {
319,830✔
1352
            return ComponentType.instance;
19,063✔
1353
        } else if (typeDescriptorLower === 'nodearray') {
300,767✔
1354
            return new ArrayType(ComponentType.instance);
1✔
1355
        } else if (typeDescriptorLower === 'rect2d') {
300,766✔
1356
            return getRect2dType();
6,356✔
1357
        } else if (typeDescriptorLower === 'rect2darray') {
294,410✔
1358
            return new ArrayType(getRect2dType());
2✔
1359
        } else if (typeDescriptorLower === 'font') {
294,408✔
1360
            return this.getNodeFieldType('roSGNodeFont', lookupTable);
46,598✔
1361
        } else if (typeDescriptorLower === 'contentnode') {
247,810✔
1362
            return this.getNodeFieldType('roSGNodeContentNode', lookupTable);
42,360✔
1363
        } else if (typeDescriptorLower.endsWith(' node')) {
205,450✔
1364
            return this.getNodeFieldType('roSgNode' + typeDescriptorLower.substring(0, typeDescriptorLower.length - 5), lookupTable);
44,478✔
1365
        } else if (lookupTable) {
160,972!
1366
            //try doing a lookup
1367
            return lookupTable.getSymbolType(typeDescriptorLower, {
160,972✔
1368
                flags: SymbolTypeFlag.typetime,
1369
                fullName: typeDescriptor,
1370
                tableProvider: () => lookupTable
38✔
1371
            });
1372
        }
1373

UNCOV
1374
        return DynamicType.instance;
×
1375
    }
1376

1377
    /**
1378
     * Return the type of the result of a binary operator
1379
     * Note: compound assignments (eg. +=) internally use a binary expression, so that's why TokenKind.PlusEqual, etc. are here too
1380
     */
1381
    public binaryOperatorResultType(leftType: BscType, operator: Token, rightType: BscType): BscType {
1382
        if ((isAnyReferenceType(leftType) && !leftType.isResolvable()) ||
772✔
1383
            (isAnyReferenceType(rightType) && !rightType.isResolvable())) {
1384
            return new BinaryOperatorReferenceType(leftType, operator, rightType, (lhs, op, rhs) => {
32✔
1385
                return this.binaryOperatorResultType(lhs, op, rhs);
5✔
1386
            });
1387
        }
1388

1389
        // Try to find a common value of union type
1390
        leftType = getUniqueType([leftType], unionTypeFactory);
740✔
1391
        rightType = getUniqueType([rightType], unionTypeFactory);
740✔
1392

1393
        if (isUnionType(leftType)) {
740✔
1394
            leftType = this.getHighestPriorityType(leftType.types);
8✔
1395
        }
1396
        if (isUnionType(rightType)) {
740✔
1397
            rightType = this.getHighestPriorityType(rightType.types);
4✔
1398
        }
1399

1400
        if (isVoidType(leftType) || isVoidType(rightType) || isUninitializedType(leftType) || isUninitializedType(rightType)) {
740✔
1401
            return undefined;
1✔
1402
        }
1403

1404
        if (isEnumMemberType(leftType)) {
739✔
1405
            leftType = leftType.underlyingType;
9✔
1406
        }
1407
        if (isEnumMemberType(rightType)) {
739✔
1408
            rightType = rightType.underlyingType;
8✔
1409
        }
1410

1411
        // treat object type like dynamic
1412
        if (isObjectType(leftType)) {
739✔
1413
            leftType = DynamicType.instance;
4✔
1414
        }
1415
        if (isObjectType(rightType)) {
739!
UNCOV
1416
            rightType = DynamicType.instance;
×
1417
        }
1418

1419
        let hasDouble = isDoubleTypeLike(leftType) || isDoubleTypeLike(rightType);
739✔
1420
        let hasFloat = isFloatTypeLike(leftType) || isFloatTypeLike(rightType);
739✔
1421
        let hasLongInteger = isLongIntegerTypeLike(leftType) || isLongIntegerTypeLike(rightType);
739✔
1422
        let hasInvalid = isInvalidTypeLike(leftType) || isInvalidTypeLike(rightType);
739✔
1423
        let hasDynamic = isDynamicType(leftType) || isDynamicType(rightType);
739✔
1424
        let bothDynamic = isDynamicType(leftType) && isDynamicType(rightType);
739✔
1425
        let bothNumbers = isNumberTypeLike(leftType) && isNumberTypeLike(rightType);
739✔
1426
        let hasNumber = isNumberTypeLike(leftType) || isNumberTypeLike(rightType);
739✔
1427
        let bothStrings = isStringTypeLike(leftType) && isStringTypeLike(rightType);
739✔
1428
        let hasString = isStringTypeLike(leftType) || isStringTypeLike(rightType);
739✔
1429
        let hasBoolean = isBooleanTypeLike(leftType) || isBooleanTypeLike(rightType);
739✔
1430
        let eitherBooleanOrNum = (isNumberTypeLike(leftType) || isBooleanTypeLike(leftType)) && (isNumberTypeLike(rightType) || isBooleanTypeLike(rightType));
739✔
1431

1432
        let leftIsPrimitive = isPrimitiveType(leftType);
739✔
1433
        let rightIsPrimitive = isPrimitiveType(rightType);
739✔
1434
        let hasPrimitive = leftIsPrimitive || rightIsPrimitive;
739✔
1435

1436
        let nonDynamicType: BscType;
1437
        if (hasPrimitive) {
739✔
1438
            nonDynamicType = leftIsPrimitive ? leftType : rightType;
661✔
1439
        }
1440

1441
        // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
1442
        switch (operator.kind) {
739✔
1443
            // Math operators
1444
            case TokenKind.Plus:
2,315✔
1445
            case TokenKind.PlusEqual:
1446
                if (bothStrings) {
301✔
1447
                    // "string" + "string" is the only binary expression allowed with strings
1448
                    return StringType.instance;
137✔
1449
                } else if (hasString && hasDynamic) {
164✔
1450
                    // assume dynamicValue is a string
1451
                    return StringType.instance;
9✔
1452
                }
1453
            // eslint-disable-next-line no-fallthrough
1454
            case TokenKind.Minus:
1455
            case TokenKind.MinusEqual:
1456
            case TokenKind.Star:
1457
            case TokenKind.StarEqual:
1458
            case TokenKind.Mod:
1459
                if (bothNumbers) {
251✔
1460
                    if (hasDouble) {
181✔
1461
                        return DoubleType.instance;
6✔
1462
                    } else if (hasFloat) {
175✔
1463
                        return FloatType.instance;
31✔
1464

1465
                    } else if (hasLongInteger) {
144✔
1466
                        return LongIntegerType.instance;
27✔
1467
                    }
1468
                    return IntegerType.instance;
117✔
1469
                } else if (hasNumber && hasDynamic) {
70✔
1470
                    // assume dynamic is a number
1471
                    return nonDynamicType;
37✔
1472
                }
1473
                break;
33✔
1474
            case TokenKind.Forwardslash:
1475
            case TokenKind.ForwardslashEqual:
1476
                if (bothNumbers) {
17✔
1477
                    if (hasDouble) {
12✔
1478
                        return DoubleType.instance;
1✔
1479
                    } else if (hasFloat) {
11✔
1480
                        return FloatType.instance;
1✔
1481

1482
                    } else if (hasLongInteger) {
10✔
1483
                        return LongIntegerType.instance;
5✔
1484
                    }
1485
                    return FloatType.instance;
5✔
1486
                } else if (hasNumber && hasDynamic) {
5!
1487
                    // assume dynamic is a number
1488
                    return nonDynamicType;
5✔
1489
                }
UNCOV
1490
                break;
×
1491
            case TokenKind.Backslash:
1492
            case TokenKind.BackslashEqual:
1493
                if (bothNumbers) {
15✔
1494
                    if (hasLongInteger) {
7✔
1495
                        return LongIntegerType.instance;
3✔
1496
                    }
1497
                    return IntegerType.instance;
4✔
1498
                } else if (hasNumber && hasDynamic) {
8!
1499
                    // assume dynamic is a number
1500
                    return IntegerType.instance;
8✔
1501
                }
UNCOV
1502
                break;
×
1503
            case TokenKind.Caret:
1504
                if (bothNumbers) {
22✔
1505
                    if (hasDouble || hasLongInteger) {
19✔
1506
                        return DoubleType.instance;
5✔
1507
                    } else if (hasFloat) {
14✔
1508
                        return FloatType.instance;
2✔
1509
                    }
1510
                    return IntegerType.instance;
12✔
1511
                } else if (hasNumber && hasDynamic) {
3!
1512
                    // assume dynamic is a number
1513
                    return IntegerType.instance;
3✔
1514
                }
UNCOV
1515
                break;
×
1516
            // Bitshift operators
1517
            case TokenKind.LeftShift:
1518
            case TokenKind.LeftShiftEqual:
1519
            case TokenKind.RightShift:
1520
            case TokenKind.RightShiftEqual:
1521
                if (bothNumbers) {
30✔
1522
                    if (hasLongInteger) {
22✔
1523
                        return LongIntegerType.instance;
2✔
1524
                    }
1525
                    // Bitshifts are allowed with non-integer numerics
1526
                    // but will always truncate to ints
1527
                    return IntegerType.instance;
20✔
1528
                } else if (hasNumber && hasDynamic) {
8!
1529
                    // assume dynamic is a number
1530
                    return IntegerType.instance;
8✔
1531
                }
UNCOV
1532
                break;
×
1533
            // Comparison operators
1534
            // All comparison operators result in boolean
1535
            case TokenKind.Equal:
1536
            case TokenKind.LessGreater:
1537
                // = and <> can accept invalid / dynamic
1538
                if (hasDynamic || hasInvalid || bothStrings || eitherBooleanOrNum) {
144✔
1539
                    return BooleanType.instance;
141✔
1540
                }
1541
                break;
3✔
1542
            case TokenKind.Greater:
1543
            case TokenKind.Less:
1544
            case TokenKind.GreaterEqual:
1545
            case TokenKind.LessEqual:
1546
                if (bothStrings || bothNumbers) {
50✔
1547
                    return BooleanType.instance;
31✔
1548
                } else if ((hasNumber || hasString) && hasDynamic) {
19!
1549
                    // assume dynamic is a valid type
1550
                    return BooleanType.instance;
18✔
1551
                }
1552
                break;
1✔
1553
            // Logical or bitwise operators
1554
            case TokenKind.Or:
1555
            case TokenKind.And:
1556
                if (bothNumbers) {
64✔
1557
                    // "and"/"or" represent bitwise operators
1558
                    if (hasLongInteger && !hasDouble && !hasFloat) {
14✔
1559
                        // 2 long ints or long int and int
1560
                        return LongIntegerType.instance;
3✔
1561
                    }
1562
                    return IntegerType.instance;
11✔
1563
                } else if (eitherBooleanOrNum) {
50✔
1564
                    // "and"/"or" represent logical operators
1565
                    return BooleanType.instance;
39✔
1566
                } else if (hasNumber && hasDynamic) {
11✔
1567
                    // assume dynamic is a valid type
1568
                    return IntegerType.instance;
7✔
1569
                } else if (hasBoolean && hasDynamic) {
4✔
1570
                    // assume dynamic is a valid type
1571
                    return BooleanType.instance;
1✔
1572
                }
1573
                break;
3✔
1574
        }
1575
        if (bothDynamic) {
40✔
1576
            return DynamicType.instance;
17✔
1577
        }
1578
        return undefined;
23✔
1579
    }
1580

1581
    public getHighestPriorityType(types: BscType[], depth = 0): BscType {
14✔
1582
        let result: BscType;
1583
        if (depth > 4) {
14!
1584
            // shortcut for very complicated types, or self-referencing union types
UNCOV
1585
            return DynamicType.instance;
×
1586
        }
1587
        for (let type of types) {
14✔
1588
            if (isUnionType(type)) {
30!
UNCOV
1589
                type = getUniqueType([type], unionTypeFactory);
×
1590
                if (isUnionType(type)) {
×
1591
                    type = this.getHighestPriorityType(type.types, depth + 1);
×
1592
                }
1593
            }
1594
            if (!result) {
30✔
1595
                result = type;
14✔
1596
            } else {
1597
                if (type.binaryOpPriorityLevel < result.binaryOpPriorityLevel) {
16✔
1598
                    result = type;
12✔
1599
                } else if (type.binaryOpPriorityLevel === result.binaryOpPriorityLevel && !result.isEqual(type)) {
4✔
1600
                    // equal priority types, but not equal types, like Boolean and String... just be dynamic at this point
1601
                    result = DynamicType.instance;
2✔
1602
                }
1603
            }
1604
            if (isUninitializedType(type)) {
30!
UNCOV
1605
                return type;
×
1606
            }
1607
            if (isVoidType(type)) {
30!
UNCOV
1608
                return type;
×
1609
            }
1610
            if (isInvalidTypeLike(type)) {
30!
UNCOV
1611
                return type;
×
1612
            }
1613
            if (isObjectType(type) && !isDynamicType(type)) {
30!
UNCOV
1614
                result = type;
×
1615
            }
1616
            if (isDynamicType(type)) {
30✔
1617
                result = type;
3✔
1618
            }
1619
        }
1620
        return result ?? DynamicType.instance;
14!
1621
    }
1622

1623
    /**
1624
     * Return the type of the result of a unary operator
1625
     */
1626
    public unaryOperatorResultType(operator: Token, exprType: BscType): BscType {
1627

1628
        if (isUnionType(exprType)) {
96✔
1629
            exprType = this.getHighestPriorityType(exprType.types);
2✔
1630
        }
1631

1632
        if (isVoidType(exprType) || isInvalidTypeLike(exprType) || isUninitializedType(exprType)) {
96✔
1633
            return undefined;
2✔
1634
        }
1635

1636

1637
        if (isDynamicType(exprType) || isObjectType(exprType)) {
94✔
1638
            return exprType;
7✔
1639
        }
1640

1641
        // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
1642
        switch (operator.kind) {
87✔
1643
            // Math operators
1644
            case TokenKind.Plus: // (`num = +num` is valid syntax)
90✔
1645
            case TokenKind.Minus:
1646
                if (isNumberTypeLike(exprType)) {
60✔
1647
                    // a negative number will be the same type, eg, double->double, int->int, etc.
1648
                    return this.getUnboxedType(exprType);
55✔
1649
                }
1650
                break;
5✔
1651
            case TokenKind.Not:
1652
                if (isBooleanTypeLike(exprType)) {
27✔
1653
                    return BooleanType.instance;
15✔
1654
                } else if (isNumberTypeLike(exprType)) {
12✔
1655
                    //numbers can be "notted"
1656
                    // by default they go to ints, except longints, which stay that way
1657
                    if (isLongIntegerTypeLike(exprType)) {
11✔
1658
                        return LongIntegerType.instance;
1✔
1659
                    }
1660
                    return IntegerType.instance;
10✔
1661
                }
1662
                break;
1✔
1663
        }
1664
        return undefined;
6✔
1665
    }
1666

1667
    public getUnboxedType(boxedType: BscType): BscType {
1668
        if (isIntegerTypeLike(boxedType)) {
55✔
1669
            return IntegerType.instance;
40✔
1670
        } else if (isLongIntegerTypeLike(boxedType)) {
15!
UNCOV
1671
            return LongIntegerType.instance;
×
1672
        } else if (isFloatTypeLike(boxedType)) {
15✔
1673
            return FloatType.instance;
14✔
1674
        } else if (isDoubleTypeLike(boxedType)) {
1!
1675
            return DoubleType.instance;
1✔
UNCOV
1676
        } else if (isBooleanTypeLike(boxedType)) {
×
1677
            return BooleanType.instance;
×
1678
        } else if (isStringTypeLike(boxedType)) {
×
1679
            return StringType.instance;
×
1680
        } else if (isInvalidTypeLike(boxedType)) {
×
1681
            return InvalidType.instance;
×
1682
        }
UNCOV
1683
        return boxedType;
×
1684
    }
1685

1686
    /**
1687
     * Get the extension for the given file path. Basically the part after the final dot, except for
1688
     * `d.bs` which is treated as single extension
1689
     * @returns the file extension (i.e. ".d.bs", ".bs", ".brs", ".xml", ".jpg", etc...)
1690
     */
1691
    public getExtension(filePath: string) {
1692
        filePath = filePath.toLowerCase();
3,093✔
1693
        if (filePath.endsWith('.d.bs')) {
3,093✔
1694
            return '.d.bs';
34✔
1695
        } else {
1696
            return path.extname(filePath).toLowerCase();
3,059✔
1697
        }
1698
    }
1699

1700
    /**
1701
     * Load and return the list of plugins
1702
     */
1703
    public loadPlugins(cwd: string, pathOrModules: string[], onError?: (pathOrModule: string, err: Error) => void): Plugin[] {
1704
        const logger = createLogger();
130✔
1705
        return pathOrModules.reduce<Plugin[]>((acc, pathOrModule) => {
130✔
1706
            if (typeof pathOrModule === 'string') {
8!
1707
                try {
8✔
1708
                    const loaded = requireRelative(pathOrModule, cwd);
8✔
1709
                    const theExport: Plugin | PluginFactory = loaded.default ? loaded.default : loaded;
8✔
1710

1711
                    let plugin: Plugin | undefined;
1712

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

1718
                        // the official plugin format is a factory function that returns a new instance of a plugin.
1719
                    } else if (typeof theExport === 'function') {
6!
1720
                        plugin = theExport({
6✔
1721
                            version: this.getBrighterScriptVersion()
1722
                        });
1723
                    } else {
1724
                        //this should never happen; somehow an invalid plugin has made it into here
UNCOV
1725
                        throw new Error(`TILT: Encountered an invalid plugin: ${String(plugin)}`);
×
1726
                    }
1727

1728
                    if (!plugin.name) {
8✔
1729
                        plugin.name = pathOrModule;
1✔
1730
                    }
1731
                    acc.push(plugin);
8✔
1732
                } catch (err: any) {
UNCOV
1733
                    if (onError) {
×
1734
                        onError(pathOrModule, err);
×
1735
                    } else {
UNCOV
1736
                        throw err;
×
1737
                    }
1738
                }
1739
            }
1740
            return acc;
8✔
1741
        }, []);
1742
    }
1743

1744
    /**
1745
     * Gathers expressions, variables, and unique names from an expression.
1746
     * This is mostly used for the ternary expression
1747
     */
1748
    public getExpressionInfo(expression: Expression, file: BrsFile): ExpressionInfo {
1749
        const expressions = [expression];
66✔
1750
        const variableExpressions = [] as VariableExpression[];
66✔
1751
        const uniqueVarNames = new Set<string>();
66✔
1752

1753
        function expressionWalker(expression) {
1754
            if (isExpression(expression)) {
170✔
1755
                expressions.push(expression);
166✔
1756
            }
1757
            if (isVariableExpression(expression)) {
170✔
1758
                variableExpressions.push(expression);
55✔
1759
                uniqueVarNames.add(expression.tokens.name.text);
55✔
1760
            }
1761
        }
1762

1763
        // Collect all expressions. Most of these expressions are fairly small so this should be quick!
1764
        // This should only be called during transpile time and only when we actually need it.
1765
        expression?.walk(expressionWalker, {
66✔
1766
            walkMode: WalkMode.visitExpressions
1767
        });
1768

1769
        //handle the expression itself (for situations when expression is a VariableExpression)
1770
        expressionWalker(expression);
66✔
1771

1772
        const scope = file.program.getFirstScopeForFile(file);
66✔
1773
        let filteredVarNames = [...uniqueVarNames];
66✔
1774
        if (scope) {
66!
1775
            filteredVarNames = filteredVarNames.filter((varName: string) => {
66✔
1776
                const varNameLower = varName.toLowerCase();
53✔
1777
                // TODO: include namespaces in this filter
1778
                return !scope.getEnumMap().has(varNameLower) &&
53✔
1779
                    !scope.getConstMap().has(varNameLower);
1780
            });
1781
        }
1782

1783
        return { expressions: expressions, varExpressions: variableExpressions, uniqueVarNames: filteredVarNames };
66✔
1784
    }
1785

1786

1787
    public concatAnnotationLeadingTrivia(stmt: Statement): Token[] {
1788
        return [...(stmt.annotations?.map(anno => anno.leadingTrivia ?? []).flat() ?? []), ...stmt.leadingTrivia];
168!
1789
    }
1790

1791
    /**
1792
     * Create a SourceNode that maps every line to itself. Useful for creating maps for files
1793
     * that haven't changed at all, but we still need the map
1794
     */
1795
    public simpleMap(source: string, src: string) {
1796
        //create a source map from the original source code
1797
        let chunks = [] as (SourceNode | string)[];
7✔
1798
        let lines = src.split(/\r?\n/g);
7✔
1799
        for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
7✔
1800
            let line = lines[lineIndex];
27✔
1801
            chunks.push(
27✔
1802
                lineIndex > 0 ? '\n' : '',
27✔
1803
                new SourceNode(lineIndex + 1, 0, source, line)
1804
            );
1805
        }
1806
        return new SourceNode(null, null, source, chunks);
7✔
1807
    }
1808

1809
    private isWindows = process.platform === 'win32';
1✔
1810
    private standardizePathCache = new Map<string, string>();
1✔
1811

1812
    /**
1813
     * Converts a path into a standardized format (drive letter to lower, remove extra slashes, use single slash type, resolve relative parts, etc...)
1814
     */
1815
    public standardizePath(thePath: string): string {
1816
        //if we have the value in cache already, return it
1817
        if (this.standardizePathCache.has(thePath)) {
35,958✔
1818
            return this.standardizePathCache.get(thePath);
33,548✔
1819
        }
1820
        const originalPath = thePath;
2,410✔
1821

1822
        if (typeof thePath !== 'string') {
2,410!
UNCOV
1823
            return thePath;
×
1824
        }
1825

1826
        //windows path.normalize will convert all slashes to backslashes and remove duplicates
1827
        if (this.isWindows) {
2,410✔
1828
            thePath = path.win32.normalize(thePath);
15✔
1829
        } else {
1830
            //replace all windows or consecutive slashes with path.sep
1831
            thePath = thePath.replace(/[\/\\]+/g, '/');
2,395✔
1832

1833
            // only use path.normalize if dots are present since it's expensive
1834
            if (thePath.includes('./')) {
2,395✔
1835
                thePath = path.posix.normalize(thePath);
6✔
1836
            }
1837
        }
1838

1839
        // Lowercase drive letter on Windows-like paths (e.g., "C:/...")
1840
        if (thePath.charCodeAt(1) === 58 /* : */) {
2,410✔
1841
            // eslint-disable-next-line no-var
1842
            var firstChar = thePath.charCodeAt(0);
19✔
1843
            if (firstChar >= 65 && firstChar <= 90) {
19✔
1844
                thePath = String.fromCharCode(firstChar + 32) + thePath.slice(1);
3✔
1845
            }
1846
        }
1847
        this.standardizePathCache.set(originalPath, thePath);
2,410✔
1848
        return thePath;
2,410✔
1849
    }
1850

1851
    /**
1852
     * Given a Diagnostic or BsDiagnostic, return a deep clone of the diagnostic.
1853
     * @param diagnostic the diagnostic to clone
1854
     * @param relatedInformationFallbackLocation a default location to use for all `relatedInformation` entries that are missing a location
1855
     */
1856
    public toDiagnostic(diagnostic: Diagnostic | BsDiagnostic, relatedInformationFallbackLocation: string): Diagnostic {
1857
        let relatedInformation = diagnostic.relatedInformation ?? [];
138✔
1858
        if (relatedInformation.length > MAX_RELATED_INFOS_COUNT) {
138!
UNCOV
1859
            const relatedInfoLength = relatedInformation.length;
×
1860
            relatedInformation = relatedInformation.slice(0, MAX_RELATED_INFOS_COUNT);
×
1861
            relatedInformation.push({
×
1862
                message: `...and ${relatedInfoLength - MAX_RELATED_INFOS_COUNT} more`,
1863
                location: util.createLocationFromRange('   ', util.createRange(0, 0, 0, 0))
1864
            });
1865
        }
1866

1867
        const range = (diagnostic as BsDiagnostic).location?.range ??
138✔
1868
            (diagnostic as Diagnostic).range;
1869

1870
        let result = {
138✔
1871
            severity: diagnostic.severity,
1872
            range: range,
1873
            message: diagnostic.message,
1874
            relatedInformation: relatedInformation.map(x => {
1875

1876
                //clone related information just in case a plugin added circular ref info here
1877
                const clone = { ...x };
42✔
1878
                if (!clone.location) {
42✔
1879
                    // use the fallback location if available
1880
                    if (relatedInformationFallbackLocation) {
2✔
1881
                        clone.location = util.createLocationFromRange(relatedInformationFallbackLocation, range);
1✔
1882
                    } else {
1883
                        //remove this related information so it doesn't bring crash the language server
1884
                        return undefined;
1✔
1885
                    }
1886
                }
1887
                return clone;
41✔
1888
                //filter out null relatedInformation items
1889
            }).filter((x): x is DiagnosticRelatedInformation => Boolean(x)),
42✔
1890
            code: diagnostic.code ? diagnostic.code : (diagnostic as BsDiagnostic).legacyCode,
138✔
1891
            source: diagnostic.source ?? 'brs'
414✔
1892
        } as Diagnostic;
1893
        if (diagnostic?.tags?.length > 0) {
138!
UNCOV
1894
            result.tags = diagnostic.tags;
×
1895
        }
1896
        return result;
138✔
1897
    }
1898

1899
    /**
1900
     * Sort an array of objects that have a Range
1901
     */
1902
    public sortByRange<T extends { range: Range | undefined }>(locatables: T[]) {
1903
        //sort the tokens by range
1904
        return locatables?.sort((a, b) => {
31!
1905
            //handle undefined tokens to prevent crashes
1906
            if (!a?.range) {
252!
1907
                return 1;
1✔
1908
            }
1909
            if (!b?.range) {
251!
UNCOV
1910
                return -1;
×
1911
            }
1912

1913
            //start line
1914
            if (a.range.start.line < b.range.start.line) {
251!
UNCOV
1915
                return -1;
×
1916
            }
1917
            if (a.range.start.line > b.range.start.line) {
251✔
1918
                return 1;
158✔
1919
            }
1920
            //start char
1921
            if (a.range.start.character < b.range.start.character) {
93✔
1922
                return -1;
61✔
1923
            }
1924
            if (a.range.start.character > b.range.start.character) {
32!
1925
                return 1;
32✔
1926
            }
1927
            //end line
UNCOV
1928
            if (a.range.end.line < b.range.end.line) {
×
1929
                return -1;
×
1930
            }
UNCOV
1931
            if (a.range.end.line > b.range.end.line) {
×
1932
                return 1;
×
1933
            }
1934
            //end char
UNCOV
1935
            if (a.range.end.character < b.range.end.character) {
×
1936
                return -1;
×
1937
            } else if (a.range.end.character > b.range.end.character) {
×
1938
                return 1;
×
1939
            }
UNCOV
1940
            return 0;
×
1941
        });
1942
    }
1943

1944
    /**
1945
     * Wrap the given code in a markdown code fence (with the language)
1946
     */
1947
    public mdFence(code: string, language = '') {
×
1948
        return '```' + language + '\n' + code + '\n```';
157✔
1949
    }
1950

1951
    /**
1952
     * Gets each part of the dotted get.
1953
     * @param node any ast expression
1954
     * @returns an array of the parts of the dotted get. If not fully a dotted get, then returns undefined
1955
     */
1956
    public getAllDottedGetParts(node: AstNode): Identifier[] | undefined {
1957
        //this is a hot function and has been optimized. Don't rewrite unless necessary
1958
        const parts: Identifier[] = [];
13,425✔
1959
        let nextPart = node;
13,425✔
1960
        loop: while (nextPart) {
13,425✔
1961
            switch (nextPart?.kind) {
19,910!
1962
                case AstNodeKind.AssignmentStatement:
19,910!
1963
                    return [(node as AssignmentStatement).tokens.name];
13✔
1964
                case AstNodeKind.DottedGetExpression:
1965
                    parts.push((nextPart as DottedGetExpression)?.tokens.name);
6,395!
1966
                    nextPart = (nextPart as DottedGetExpression).obj;
6,395✔
1967
                    continue;
6,395✔
1968
                case AstNodeKind.CallExpression:
1969
                    nextPart = (nextPart as CallExpression).callee;
41✔
1970
                    continue;
41✔
1971
                case AstNodeKind.CallfuncExpression:
1972
                    parts.push((nextPart as CallfuncExpression)?.tokens.methodName);
17!
1973
                    nextPart = (nextPart as CallfuncExpression).callee;
17✔
1974
                    continue;
17✔
1975
                case AstNodeKind.TypeExpression:
UNCOV
1976
                    nextPart = (nextPart as TypeExpression).expression;
×
1977
                    continue;
×
1978
                case AstNodeKind.VariableExpression:
1979
                    parts.push((nextPart as VariableExpression)?.tokens.name);
13,302!
1980
                    break loop;
13,302✔
1981
                case AstNodeKind.LiteralExpression:
1982
                    parts.push((nextPart as LiteralExpression)?.tokens.value as Identifier);
12!
1983
                    break loop;
12✔
1984
                case AstNodeKind.IndexedGetExpression:
1985
                    nextPart = (nextPart as unknown as IndexedGetExpression).obj;
35✔
1986
                    continue;
35✔
1987
                case AstNodeKind.FunctionParameterExpression:
1988
                    return [(nextPart as FunctionParameterExpression).tokens.name];
8✔
1989
                case AstNodeKind.GroupingExpression:
1990
                    parts.push(createIdentifier('()', nextPart.location));
7✔
1991
                    break loop;
7✔
1992
                default:
1993
                    //we found a non-DottedGet expression, so return because this whole operation is invalid.
1994
                    return undefined;
80✔
1995
            }
1996
        }
1997
        return parts.reverse();
13,324✔
1998
    }
1999

2000
    /**
2001
     * Given an expression, return all the DottedGet name parts as a string.
2002
     * Mostly used to convert namespaced item full names to a strings
2003
     */
2004
    public getAllDottedGetPartsAsString(node: Expression | Statement, parseMode = ParseMode.BrighterScript, lastSep = '.'): string {
13,827✔
2005
        //this is a hot function and has been optimized. Don't rewrite unless necessary
2006
        /* eslint-disable no-var */
2007
        var sep = parseMode === ParseMode.BrighterScript ? '.' : '_';
11,779✔
2008

2009
        const parts = this.getAllDottedGetParts(node) ?? [];
11,779✔
2010
        var result = parts[0]?.text;
11,779✔
2011
        for (var i = 1; i < parts.length; i++) {
11,779✔
2012
            result += (i === parts.length - 1 && parseMode === ParseMode.BrighterScript ? lastSep : sep) + parts[i].text;
5,355✔
2013
        }
2014
        return result;
11,779✔
2015
        /* eslint-enable no-var */
2016
    }
2017

2018
    /**
2019
     * Break an expression into each part.
2020
     */
2021
    public splitExpression(expression: Expression) {
2022
        const parts: Expression[] = [expression];
11,577✔
2023
        let nextPart = expression;
11,577✔
2024
        while (nextPart) {
11,577✔
2025
            if (isDottedGetExpression(nextPart) || isIndexedGetExpression(nextPart) || isXmlAttributeGetExpression(nextPart)) {
14,528✔
2026
                nextPart = nextPart.obj;
1,151✔
2027

2028
            } else if (isCallExpression(nextPart) || isCallfuncExpression(nextPart)) {
13,377✔
2029
                nextPart = nextPart.callee;
1,800✔
2030

2031
            } else if (isTypeExpression(nextPart)) {
11,577!
UNCOV
2032
                nextPart = nextPart.expression;
×
2033
            } else {
2034
                break;
11,577✔
2035
            }
2036
            parts.unshift(nextPart);
2,951✔
2037
        }
2038
        return parts;
11,577✔
2039
    }
2040

2041
    /**
2042
     * Returns an integer if valid, or undefined. Eliminates checking for NaN
2043
     */
2044
    public parseInt(value: any) {
2045
        const result = parseInt(value);
34✔
2046
        if (!isNaN(result)) {
34✔
2047
            return result;
29✔
2048
        } else {
2049
            return undefined;
5✔
2050
        }
2051
    }
2052

2053
    /**
2054
     * Converts a range to a string in the format 1:2-3:4
2055
     */
2056
    public rangeToString(range: Range) {
2057
        return `${range?.start?.line}:${range?.start?.character}-${range?.end?.line}:${range?.end?.character}`;
2,139✔
2058
    }
2059

2060
    public validateTooDeepFile(file: (BrsFile | XmlFile)) {
2061
        //find any files nested too deep
2062
        let destPath = file?.destPath?.toString();
2,447!
2063
        let rootFolder = destPath?.replace(/^pkg:/, '').split(/[\\\/]/)[0].toLowerCase();
2,447!
2064

2065
        if (isBrsFile(file) && rootFolder !== 'source') {
2,447✔
2066
            return;
366✔
2067
        }
2068

2069
        if (isXmlFile(file) && rootFolder !== 'components') {
2,081!
UNCOV
2070
            return;
×
2071
        }
2072

2073
        let fileDepth = this.getParentDirectoryCount(destPath);
2,081✔
2074
        if (fileDepth >= 8) {
2,081✔
2075
            file.program?.diagnostics.register({
3!
2076
                ...DiagnosticMessages.detectedTooDeepFileSource(fileDepth),
2077
                location: util.createLocationFromFileRange(file, this.createRange(0, 0, 0, Number.MAX_VALUE))
2078
            });
2079
        }
2080
    }
2081

2082
    /**
2083
     * Execute dispose for a series of disposable items
2084
     * @param disposables a list of functions or disposables
2085
     */
2086
    public applyDispose(disposables: DisposableLike[]) {
2087
        for (const disposable of disposables ?? []) {
6!
2088
            if (typeof disposable === 'function') {
12!
2089
                disposable();
12✔
2090
            } else {
UNCOV
2091
                disposable?.dispose?.();
×
2092
            }
2093
        }
2094
    }
2095

2096
    /**
2097
     * Race a series of promises, and return the first one that resolves AND matches the matcher function.
2098
     * If all of the promises reject, then this will emit an AggregatreError with all of the errors.
2099
     * If at least one promise resolves, then this will log all of the errors to the console
2100
     * If at least one promise resolves but none of them match the matcher, then this will return undefined.
2101
     * @param promises all of the promises to race
2102
     * @param matcher a function that should return true if this value should be kept. Returning any value other than true means `false`
2103
     * @returns the first resolved value that matches the matcher, or undefined if none of them match
2104
     */
2105
    public async promiseRaceMatch<T>(promises: MaybePromise<T>[], matcher: (value: T) => boolean) {
2106
        const workingPromises = [
33✔
2107
            ...promises
2108
        ];
2109

2110
        const results: Array<{ value: T; index: number } | { error: Error; index: number }> = [];
33✔
2111
        let returnValue: T;
2112

2113
        while (workingPromises.length > 0) {
33✔
2114
            //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
2115
            const result = await Promise.race(
39✔
2116
                workingPromises.map((promise, i) => {
2117
                    return Promise.resolve(promise)
58✔
2118
                        .then(value => ({ value: value, index: i }))
50✔
2119
                        .catch(error => ({ error: error, index: i }));
7✔
2120
                })
2121
            );
2122
            results.push(result);
39✔
2123
            //if we got a value and it matches the matcher, return it
2124
            if ('value' in result && matcher?.(result.value) === true) {
39✔
2125
                returnValue = result.value;
29✔
2126
                break;
29✔
2127
            }
2128

2129
            //remove this non-matched (or errored) promise from the list and try again
2130
            workingPromises.splice(result.index, 1);
10✔
2131
        }
2132

2133
        const errors = (results as Array<{ error: Error }>)
33✔
2134
            .filter(x => 'error' in x)
39✔
2135
            .map(x => x.error);
4✔
2136

2137
        //if all of them crashed, then reject
2138
        if (promises.length > 0 && errors.length === promises.length) {
33✔
2139
            throw new AggregateError(errors, 'All requests failed. First error message: ' + errors[0].message);
1✔
2140
        } else {
2141
            //log all of the errors
2142
            for (const error of errors) {
32✔
2143
                console.error(error);
1✔
2144
            }
2145
        }
2146

2147
        //return the matched value, or undefined if there wasn't one
2148
        return returnValue;
32✔
2149
    }
2150

2151
    /**
2152
     * Wraps SourceNode's constructor to be compatible with the TranspileResult type
2153
     */
2154
    public sourceNodeFromTranspileResult(
2155
        line: number | null,
2156
        column: number | null,
2157
        source: string | null,
2158
        chunks?: string | SourceNode | TranspileResult,
2159
        name?: string
2160
    ): SourceNode {
2161
        // we can use a typecast rather than actually transforming the data because SourceNode
2162
        // accepts a more permissive type than its typedef states
2163
        return new SourceNode(line, column, source, chunks as any, name);
8,870✔
2164
    }
2165

2166
    /**
2167
     * Find the index of the last item in the array that matches.
2168
     */
2169
    public findLastIndex<T>(array: T[], matcher: (T) => boolean) {
2170
        for (let i = array.length - 1; i >= 0; i--) {
27✔
2171
            if (matcher(array[i])) {
24✔
2172
                return i;
16✔
2173
            }
2174
        }
2175
    }
2176

2177
    public processTypeChain(typeChain: TypeChainEntry[]): TypeChainProcessResult {
2178
        let fullChainName = '';
1,350✔
2179
        let fullErrorName = '';
1,350✔
2180
        let itemName = '';
1,350✔
2181
        let previousTypeName = '';
1,350✔
2182
        let parentTypeName = '';
1,350✔
2183
        let itemTypeKind = '';
1,350✔
2184
        let parentTypeKind = '';
1,350✔
2185
        let astNode: AstNode;
2186
        let errorLocation: Location;
2187
        let containsDynamic = false;
1,350✔
2188
        let continueResolvingAllItems = true;
1,350✔
2189
        let crossedCallFunc = false;
1,350✔
2190
        for (let i = 0; i < typeChain.length; i++) {
1,350✔
2191
            const chainItem = typeChain[i];
2,487✔
2192
            const dotSep = chainItem.separatorToken?.text ?? '.';
2,487!
2193
            if (i > 0) {
2,487✔
2194
                fullChainName += dotSep;
1,152✔
2195
            }
2196
            fullChainName += chainItem.name;
2,487✔
2197
            if (continueResolvingAllItems) {
2,487✔
2198
                parentTypeName = previousTypeName;
2,265✔
2199
                parentTypeKind = itemTypeKind;
2,265✔
2200
                fullErrorName = previousTypeName ? `${previousTypeName}${dotSep}${chainItem.name}` : chainItem.name;
2,265✔
2201
                itemTypeKind = (chainItem.type as any)?.kind;
2,265✔
2202

2203
                let typeString = chainItem.type?.toString();
2,265✔
2204
                let typeToFindStringFor = chainItem.type;
2,265✔
2205
                while (typeToFindStringFor) {
2,265✔
2206
                    if (isUnionType(chainItem.type)) {
2,248✔
2207
                        typeString = `(${typeToFindStringFor.toString()})`;
5✔
2208
                        break;
5✔
2209
                    } else if (isCallableType(typeToFindStringFor)) {
2,243✔
2210
                        if (isTypedFunctionType(typeToFindStringFor) && i < typeChain.length - 1) {
102✔
2211
                            typeToFindStringFor = typeToFindStringFor.returnType;
9✔
2212
                        } else {
2213
                            typeString = 'function';
93✔
2214
                            break;
93✔
2215
                        }
2216
                        parentTypeName = previousTypeName;
9✔
2217
                    } else if (isNamespaceType(typeToFindStringFor) && parentTypeName) {
2,141✔
2218
                        const chainItemTypeName = typeToFindStringFor.toString();
332✔
2219
                        typeString = parentTypeName + '.' + chainItemTypeName;
332✔
2220
                        if (chainItemTypeName.toLowerCase().startsWith(parentTypeName.toLowerCase())) {
332!
2221
                            // the following namespace already knows...
2222
                            typeString = chainItemTypeName;
332✔
2223
                        }
2224
                        break;
332✔
2225
                    } else {
2226
                        typeString = typeToFindStringFor?.toString();
1,809!
2227
                        break;
1,809✔
2228
                    }
2229
                }
2230

2231
                previousTypeName = typeString ?? '';
2,265✔
2232
                itemName = chainItem.name;
2,265✔
2233
                astNode = chainItem.astNode;
2,265✔
2234
                containsDynamic = containsDynamic || (isDynamicType(chainItem.type) && !isAnyReferenceType(chainItem.type));
2,265✔
2235
                crossedCallFunc = crossedCallFunc || chainItem.data?.isFromCallFunc;
2,265!
2236
                if (!chainItem.isResolved) {
2,265✔
2237
                    errorLocation = chainItem.location;
1,163✔
2238
                    continueResolvingAllItems = false;
1,163✔
2239
                }
2240
            }
2241
        }
2242
        return {
1,350✔
2243
            itemName: itemName,
2244
            itemTypeKind: itemTypeKind,
2245
            itemParentTypeName: parentTypeName,
2246
            itemParentTypeKind: parentTypeKind,
2247
            fullNameOfItem: fullErrorName,
2248
            fullChainName: fullChainName,
2249
            location: errorLocation,
2250
            containsDynamic: containsDynamic,
2251
            astNode: astNode,
2252
            crossedCallFunc: crossedCallFunc
2253
        };
2254
    }
2255

2256
    public getCircularReferenceDiagnosticDetail(circularReferenceInfo: TypeCircularReferenceInfo, defaultName = ''): string[] {
×
2257
        if (!circularReferenceInfo || !circularReferenceInfo.referenceChainNames) {
16!
UNCOV
2258
            if (defaultName) {
×
2259
                return [defaultName];
×
2260
            }
UNCOV
2261
            return [];
×
2262
        }
2263

2264
        if (circularReferenceInfo.referenceChainNames[0] === circularReferenceInfo.referenceChainNames[circularReferenceInfo.referenceChainNames.length - 1]) {
16✔
2265
            // last element is same as first it will read okay
2266
            return circularReferenceInfo.referenceChainNames;
2✔
2267
        }
2268
        return [...circularReferenceInfo.referenceChainNames, circularReferenceInfo.referenceChainNames[0]];
14✔
2269
    }
2270

2271

2272
    public isInTypeExpression(expression: AstNode): boolean {
2273
        //TODO: this is much faster than node.findAncestor(), but may need to be updated for "complicated" type expressions
2274
        if (isTypeExpression(expression) ||
18,540✔
2275
            isTypeExpression(expression?.parent) ||
55,095!
2276
            isTypedArrayExpression(expression) ||
2277
            isTypedArrayExpression(expression?.parent)) {
42,222!
2278
            return true;
4,552✔
2279
        }
2280
        if (isBinaryExpression(expression?.parent)) {
13,988!
2281
            let currentExpr: AstNode = expression.parent;
2,888✔
2282
            while (isBinaryExpression(currentExpr) && currentExpr.tokens.operator.kind === TokenKind.Or) {
2,888✔
2283
                currentExpr = currentExpr.parent;
351✔
2284
            }
2285
            return isTypeExpression(currentExpr) || isTypedArrayExpression(currentExpr);
2,888✔
2286
        }
2287
        return false;
11,100✔
2288
    }
2289

2290
    public isGenericNodeType(type: BscType) {
2291
        if (isComponentType(type)) {
53✔
2292
            const lowerName = type.toString().toLowerCase();
36✔
2293
            if (lowerName === 'rosgnode' || lowerName === 'rosgnodenode') {
36✔
2294
                return true;
8✔
2295
            }
2296
        }
2297
        return false;
45✔
2298
    }
2299

2300

2301
    public hasAnyRequiredSymbolChanged(requiredSymbols: UnresolvedSymbol[], changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
2302
        if (!requiredSymbols || !changedSymbols) {
2,008!
UNCOV
2303
            return false;
×
2304
        }
2305
        const runTimeChanges = changedSymbols.get(SymbolTypeFlag.runtime);
2,008✔
2306
        const typeTimeChanges = changedSymbols.get(SymbolTypeFlag.typetime);
2,008✔
2307

2308
        for (const symbol of requiredSymbols) {
2,008✔
2309
            if (this.setContainsUnresolvedSymbol(runTimeChanges, symbol) || this.setContainsUnresolvedSymbol(typeTimeChanges, symbol)) {
572✔
2310
                return true;
444✔
2311
            }
2312
        }
2313

2314
        return false;
1,564✔
2315
    }
2316

2317
    public setContainsUnresolvedSymbol(symbolLowerNameSet: Set<string>, symbol: UnresolvedSymbol) {
2318
        if (!symbolLowerNameSet || symbolLowerNameSet.size === 0) {
790✔
2319
            return false;
89✔
2320
        }
2321

2322
        for (const possibleNameLower of symbol.lookups) {
701✔
2323
            if (symbolLowerNameSet.has(possibleNameLower)) {
1,750✔
2324
                return true;
444✔
2325
            }
2326
        }
2327
        return false;
257✔
2328
    }
2329

2330
    public getCustomTypesInSymbolTree(setToFill: Set<BscType>, type: BscType, filter?: (t: BscSymbol) => boolean) {
2331
        const subSymbols = type.getMemberTable()?.getAllSymbols(SymbolTypeFlag.runtime) ?? [];
1,180!
2332
        for (const subSymbol of subSymbols) {
1,180✔
2333
            if (!subSymbol.type?.isBuiltIn && !setToFill.has(subSymbol.type)) {
6,064!
2334
                if (filter && !filter(subSymbol)) {
405!
UNCOV
2335
                    continue;
×
2336
                }
2337
                // 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
2338
                // add the type, and investigate any members
2339
                setToFill.add(subSymbol.type);
405✔
2340
                this.getCustomTypesInSymbolTree(setToFill, subSymbol.type, filter);
405✔
2341
            }
2342

2343
        }
2344
    }
2345

2346
    public truncate<T>(options: {
2347
        leadingText: string;
2348
        items: T[];
2349
        trailingText?: string;
2350
        maxLength: number;
2351
        itemSeparator?: string;
2352
        partBuilder?: (item: T) => string;
2353
    }): string {
2354
        let leadingText = options.leadingText;
31✔
2355
        let items = options?.items ?? [];
31!
2356
        let trailingText = options?.trailingText ?? '';
31!
2357
        let maxLength = options?.maxLength ?? 160;
31!
2358
        let itemSeparator = options?.itemSeparator ?? ', ';
31!
2359
        let partBuilder = options?.partBuilder ?? ((x) => x.toString());
31!
2360

2361
        let parts = [];
31✔
2362
        let length = leadingText.length + (trailingText?.length ?? 0);
31!
2363

2364
        //calculate the max number of items we could fit in the given space
2365
        for (let i = 0; i < items.length; i++) {
31✔
2366
            let part = partBuilder(items[i]);
103✔
2367
            if (i > 0) {
103✔
2368
                part = itemSeparator + part;
72✔
2369
            }
2370
            parts.push(part);
103✔
2371
            length += part.length;
103✔
2372
            //exit the loop if we've maxed out our length
2373
            if (length >= maxLength) {
103✔
2374
                break;
6✔
2375
            }
2376
        }
2377
        let message: string;
2378
        //we have enough space to include all the parts
2379
        if (parts.length >= items.length) {
31✔
2380
            message = leadingText + parts.join('') + trailingText;
25✔
2381

2382
            //we require truncation
2383
        } else {
2384
            //account for truncation message length including max possible "more" items digits, trailing text length, and the separator between last item and trailing text
2385
            length = leadingText.length + `...and ${items.length} more`.length + itemSeparator.length + (trailingText?.length ?? 0);
6!
2386
            message = leadingText;
6✔
2387
            for (let i = 0; i < parts.length; i++) {
6✔
2388
                //always include at least 2 items. if this part would overflow the max, then skip it and finalize the message
2389
                if (i > 1 && length + parts[i].length > maxLength) {
47✔
2390
                    message += itemSeparator + `...and ${items.length - i} more` + trailingText;
6✔
2391
                    return message;
6✔
2392
                } else {
2393
                    message += parts[i];
41✔
2394
                    length += parts[i].length;
41✔
2395
                }
2396
            }
2397
        }
2398
        return message;
25✔
2399
    }
2400

2401
    public getAstNodeFriendlyName(node: AstNode) {
2402
        return node?.kind.replace(/Statement|Expression/g, '');
263!
2403
    }
2404

2405

2406
    public hasLeadingComments(input: Token | AstNode) {
2407
        const leadingTrivia = isToken(input) ? input?.leadingTrivia : input?.leadingTrivia ?? [];
8,165!
2408
        return !!leadingTrivia.find(t => t?.kind === TokenKind.Comment);
16,629✔
2409
    }
2410

2411
    public getLeadingComments(input: Token | AstNode) {
2412
        const leadingTrivia = isToken(input) ? input?.leadingTrivia : input?.leadingTrivia ?? [];
12,859!
2413
        return leadingTrivia.filter(t => t?.kind === TokenKind.Comment);
39,104✔
2414
    }
2415

2416
    public isLeadingCommentOnSameLine(line: RangeLike, input: Token | AstNode) {
2417
        const leadingCommentRange = this.getLeadingComments(input)?.[0];
11,939!
2418
        if (leadingCommentRange) {
11,939✔
2419
            return this.linesTouch(line, leadingCommentRange?.location);
1,700!
2420
        }
2421
        return false;
10,239✔
2422
    }
2423

2424
    public isClassUsedAsFunction(potentialClassType: BscType, expression: Expression, options: GetTypeOptions) {
2425
        // eslint-disable-next-line no-bitwise
2426
        if ((options?.flags ?? 0) & SymbolTypeFlag.runtime &&
28,731!
2427
            isClassType(potentialClassType) &&
2428
            !options.isExistenceTest &&
2429
            potentialClassType.name?.toLowerCase() === this.getAllDottedGetPartsAsString(expression)?.toLowerCase() &&
6,624✔
2430
            !expression?.findAncestor(isNewExpression)) {
1,308✔
2431
            return true;
32✔
2432
        }
2433
        return false;
28,699✔
2434
    }
2435

2436
    public getSpecialCaseCallExpressionReturnType(callExpr: CallExpression, options: GetSymbolTypeOptions) {
2437
        if (isVariableExpression(callExpr.callee) && callExpr.callee.tokens.name.text.toLowerCase() === 'createobject') {
734✔
2438
            const componentName = isLiteralString(callExpr.args[0]) ? callExpr.args[0].tokens.value?.text?.replace(/"/g, '') : '';
141!
2439
            const nodeType = componentName.toLowerCase() === 'rosgnode' && isLiteralString(callExpr.args[1]) ? callExpr.args[1].tokens.value?.text?.replace(/"/g, '') : '';
141!
2440
            if (componentName?.toLowerCase().startsWith('ro')) {
141!
2441
                let fullName = componentName + nodeType;
125✔
2442

2443
                if (nodeType.includes(':')) {
125✔
2444
                    // This node has a colon in its name, most likely from a component Library
2445
                    // This componentType is most likely unknown, so return `roSGNode`
2446
                    fullName = 'roSGNode';
8✔
2447
                }
2448

2449
                const data = {};
125✔
2450
                const symbolTable = callExpr.getSymbolTable();
125✔
2451
                const foundType = symbolTable.getSymbolType(fullName, {
125✔
2452
                    flags: SymbolTypeFlag.typetime,
2453
                    data: data,
2454
                    tableProvider: () => callExpr?.getSymbolTable(),
20!
2455
                    fullName: fullName
2456
                });
2457
                if (foundType) {
125!
2458
                    return foundType;
125✔
2459
                }
2460
            }
2461
        } else if (isDottedGetExpression(callExpr.callee) &&
593✔
2462
            callExpr.callee.tokens?.name?.text?.toLowerCase() === 'callfunc' &&
3,789!
2463
            isLiteralString(callExpr.args?.[0])) {
6!
2464
            return this.getCallFuncType(callExpr, callExpr.args?.[0]?.tokens.value, options);
2!
2465
        } else if (isDottedGetExpression(callExpr.callee) &&
591✔
2466
            callExpr.callee.tokens?.name?.text?.toLowerCase() === 'createchild' &&
3,771!
2467
            isComponentType(callExpr.callee.obj?.getType({ flags: SymbolTypeFlag.runtime })) &&
6!
2468
            isLiteralString(callExpr.args?.[0])) {
6!
2469
            const fullName = `roSGNode${callExpr.args?.[0].tokens?.value?.text?.replace(/"/g, '')}`;
2!
2470
            const data = {};
2✔
2471
            const symbolTable = callExpr.getSymbolTable();
2✔
2472
            return symbolTable.getSymbolType(fullName, {
2✔
2473
                flags: SymbolTypeFlag.typetime,
2474
                data: data,
UNCOV
2475
                tableProvider: () => callExpr?.getSymbolTable(),
×
2476
                fullName: fullName
2477
            });
2478
        }
2479
    }
2480

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

2485
        // a little hacky here with checking options.ignoreCall because callFuncExpression has the method name
2486
        // It's nicer for CallExpression, because it's a call on any expression.
2487
        let calleeType: BscType;
2488
        if (isCallfuncExpression(callExpr)) {
73✔
2489
            calleeType = callExpr.callee.getType({ ...options, flags: SymbolTypeFlag.runtime, ignoreCall: false });
64✔
2490
        } else if (isCallExpression(callExpr) && isDottedGetExpression(callExpr.callee)) {
9!
2491
            calleeType = callExpr.callee.obj.getType({ ...options, flags: SymbolTypeFlag.runtime, ignoreCall: false });
9✔
2492
        }
2493
        const funcType = calleeType.getCallFuncType?.(methodName, options);
73!
2494
        if (funcType) {
73✔
2495
            options.typeChain?.push(new TypeChainEntry({
66✔
2496
                name: methodName,
2497
                type: funcType,
2498
                data: options.data,
2499
                location: methodNameToken.location,
2500
                separatorToken: createToken(TokenKind.Callfunc),
2501
                astNode: callExpr
2502
            }));
2503
            if (options.ignoreCall) {
66✔
2504
                result = funcType;
38✔
2505
            } else if (isCallableType(funcType) && (!isReferenceType(funcType.returnType) || funcType.returnType.isResolvable())) {
28✔
2506
                result = funcType.returnType;
15✔
2507
            } else if (!isReferenceType(funcType) && (funcType as any)?.returnType?.isResolvable()) {
13!
2508
                result = (funcType as any).returnType;
1✔
2509
            } else if (this.isUnionOfFunctions(funcType)) {
12!
UNCOV
2510
                result = this.getReturnTypeOfUnionOfFunctions(funcType);
×
2511
            } else {
2512
                result = new TypePropertyReferenceType(funcType, 'returnType');
12✔
2513
            }
2514
        }
2515
        if (isVoidType(result)) {
73✔
2516
            // CallFunc will always return invalid, even if function called is `as void`
2517
            result = DynamicType.instance;
1✔
2518
        }
2519
        if (options.data && !options.ignoreCall) {
70✔
2520
            options.data.isFromCallFunc = true;
28✔
2521
        }
2522
        return result;
70✔
2523
    }
2524

2525
    public isUnionOfFunctions(type: BscType, allowReferenceTypes = false): type is UnionType {
26✔
2526
        if (isUnionType(type)) {
91✔
2527
            const callablesInUnion = type.types.filter(t => isCallableType(t) || (allowReferenceTypes && isReferenceType(t)));
58✔
2528
            return callablesInUnion.length === type.types.length && callablesInUnion.length > 0;
29✔
2529
        }
2530
        return false;
62✔
2531
    }
2532

2533
    public isIntersectionOfFunctions(type: BscType, allowReferenceTypes = false): type is IntersectionType {
×
NEW
2534
        if (isIntersectionType(type)) {
×
NEW
2535
            const callablesInUnion = type.types.filter(t => isCallableType(t) || (allowReferenceTypes && isReferenceType(t)));
×
NEW
2536
            return callablesInUnion.length === type.types.length && callablesInUnion.length > 0;
×
2537
        }
NEW
2538
        return false;
×
2539
    }
2540

2541
    public getFunctionTypeFromUnion(type: BscType): BscType {
2542
        if (this.isUnionOfFunctions(type)) {
6✔
2543
            const typedFuncsInUnion = type.types.filter(isTypedFunctionType);
5✔
2544
            if (typedFuncsInUnion.length < type.types.length) {
5!
2545
                // has non-typedFuncs in union
UNCOV
2546
                return FunctionType.instance;
×
2547
            }
2548
            const exampleFunc = typedFuncsInUnion[0];
5✔
2549
            const cumulativeFunction = new TypedFunctionType(getUniqueType(typedFuncsInUnion.map(f => f.returnType), (types) => new UnionType(types)))
10✔
2550
                .setName(exampleFunc.name)
2551
                .setSub(exampleFunc.isSub);
2552
            for (const param of exampleFunc.params) {
5✔
2553
                cumulativeFunction.addParameter(param.name, param.type, param.isOptional);
3✔
2554
            }
2555
            return cumulativeFunction;
5✔
2556
        }
2557
        return undefined;
1✔
2558
    }
2559

2560
    public getFunctionTypeFromIntersection(type: BscType): BscType {
NEW
2561
        if (this.isIntersectionOfFunctions(type)) {
×
NEW
2562
            const typedFuncsInUnion = type.types.filter(isTypedFunctionType);
×
NEW
2563
            if (typedFuncsInUnion.length < type.types.length) {
×
2564
                // has non-typedFuncs in union
NEW
2565
                return FunctionType.instance;
×
2566
            }
NEW
2567
            const exampleFunc = typedFuncsInUnion[0];
×
NEW
2568
            const cumulativeFunction = new TypedFunctionType(getUniqueType(typedFuncsInUnion.map(f => f.returnType), (types) => new IntersectionType(types)))
×
2569
                .setName(exampleFunc.name)
2570
                .setSub(exampleFunc.isSub);
NEW
2571
            for (const param of exampleFunc.params) {
×
NEW
2572
                cumulativeFunction.addParameter(param.name, param.type, param.isOptional);
×
2573
            }
NEW
2574
            return cumulativeFunction;
×
2575
        }
NEW
2576
        return undefined;
×
2577
    }
2578

2579
    public getReturnTypeOfUnionOfFunctions(type: UnionType): BscType {
2580
        if (this.isUnionOfFunctions(type, true)) {
10!
2581
            const typedFuncsInUnion = type.types.filter(t => isTypedFunctionType(t) || isReferenceType(t)) as TypedFunctionType[];
20✔
2582
            if (typedFuncsInUnion.length < type.types.length) {
10!
2583
                // is non-typedFuncs in union
UNCOV
2584
                return DynamicType.instance;
×
2585
            }
2586
            const funcReturns = typedFuncsInUnion.map(f => f.returnType);
20✔
2587
            return getUniqueType(funcReturns, (types) => new UnionType(types));
10✔
2588
        }
2589
        return InvalidType.instance;
×
2590
    }
2591

2592
    public getReturnTypeOfIntersectionOfFunctions(type: IntersectionType): BscType {
NEW
2593
        if (this.isIntersectionOfFunctions(type, true)) {
×
NEW
2594
            const typedFuncsInUnion = type.types.filter(t => isTypedFunctionType(t) || isReferenceType(t)) as TypedFunctionType[];
×
NEW
2595
            if (typedFuncsInUnion.length < type.types.length) {
×
2596
                // is non-typedFuncs in union
NEW
2597
                return DynamicType.instance;
×
2598
            }
NEW
2599
            const funcReturns = typedFuncsInUnion.map(f => f.returnType);
×
NEW
2600
            return new IntersectionType(funcReturns);//getUniqueType(funcReturns, (types) => new UnionType(types));
×
2601
        }
NEW
2602
        return InvalidType.instance;
×
2603
    }
2604

2605
    public symbolComesFromSameNode(symbolName: string, definingNode: AstNode, symbolTable: SymbolTable) {
2606
        let nsData: ExtraSymbolData = {};
707✔
2607
        let foundType = symbolTable?.getSymbolType(symbolName, { flags: SymbolTypeFlag.runtime, data: nsData });
707!
2608
        if (foundType && definingNode === nsData?.definingNode) {
707!
2609
            return true;
304✔
2610
        }
2611
        return false;
403✔
2612
    }
2613

2614
    public isCalleeMemberOfNamespace(symbolName: string, nodeWhereUsed: AstNode, namespace?: NamespaceStatement) {
2615
        namespace = namespace ?? nodeWhereUsed.findAncestor<NamespaceStatement>(isNamespaceStatement);
74!
2616

2617
        if (!this.isVariableMemberOfNamespace(symbolName, nodeWhereUsed, namespace)) {
74✔
2618
            return false;
56✔
2619
        }
2620
        const exprType = nodeWhereUsed.getType({ flags: SymbolTypeFlag.runtime });
18✔
2621

2622
        if (isCallableType(exprType) || isClassType(exprType)) {
18!
2623
            return true;
18✔
2624
        }
2625
        return false;
×
2626
    }
2627

2628
    public isVariableMemberOfNamespace(symbolName: string, nodeWhereUsed: AstNode, namespace?: NamespaceStatement) {
2629
        namespace = namespace ?? nodeWhereUsed.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,146✔
2630
        if (!isNamespaceStatement(namespace)) {
2,146✔
2631
            return false;
1,748✔
2632
        }
2633
        const namespaceParts = namespace.getNameParts();
398✔
2634
        let namespaceType: NamespaceType;
2635
        let symbolTable: SymbolTable = namespace.getSymbolTable();
398✔
2636
        for (const part of namespaceParts) {
398✔
2637
            namespaceType = symbolTable.getSymbolType(part.text, { flags: SymbolTypeFlag.runtime }) as NamespaceType;
684✔
2638
            if (namespaceType) {
684✔
2639
                symbolTable = namespaceType.getMemberTable();
683✔
2640
            } else {
2641
                return false;
1✔
2642
            }
2643
        }
2644

2645
        let varData: ExtraSymbolData = {};
397✔
2646
        nodeWhereUsed.getType({ flags: SymbolTypeFlag.runtime, data: varData });
397✔
2647
        const isFromSameNodeInMemberTable = this.symbolComesFromSameNode(symbolName, varData?.definingNode, namespaceType?.getMemberTable());
397!
2648
        return isFromSameNodeInMemberTable;
397✔
2649
    }
2650

2651
    public isVariableShadowingSomething(symbolName: string, nodeWhereUsed: AstNode) {
2652
        let varData: ExtraSymbolData = {};
7,088✔
2653
        let exprType = nodeWhereUsed.getType({ flags: SymbolTypeFlag.runtime, data: varData });
7,088✔
2654
        if (isReferenceType(exprType)) {
7,088✔
2655
            exprType = (exprType as any).getTarget();
6,600✔
2656
        }
2657
        const namespace = nodeWhereUsed?.findAncestor<NamespaceStatement>(isNamespaceStatement);
7,088!
2658

2659
        if (isNamespaceStatement(namespace)) {
7,088✔
2660
            let namespaceHasSymbol = namespace.getSymbolTable().hasSymbol(symbolName, SymbolTypeFlag.runtime);
87✔
2661
            // check if the namespace has a symbol with the same name, but different definiton
2662
            if (namespaceHasSymbol && !this.symbolComesFromSameNode(symbolName, varData.definingNode, namespace.getSymbolTable())) {
87✔
2663
                return true;
15✔
2664
            }
2665
        }
2666
        const bodyTable = nodeWhereUsed.getRoot().getSymbolTable();
7,073✔
2667
        const hasSymbolAtFileLevel = bodyTable.hasSymbol(symbolName, SymbolTypeFlag.runtime);
7,073✔
2668
        if (hasSymbolAtFileLevel && !this.symbolComesFromSameNode(symbolName, varData.definingNode, bodyTable)) {
7,073✔
2669
            return true;
10✔
2670
        }
2671

2672
        return false;
7,063✔
2673
    }
2674

2675
    public chooseTypeFromCodeOrDocComment(codeType: BscType, docType: BscType, options: GetTypeOptions) {
2676
        let returnType: BscType;
2677
        if (options.preferDocType && docType) {
12,955!
UNCOV
2678
            returnType = docType;
×
UNCOV
2679
            if (options.data) {
×
UNCOV
2680
                options.data.isFromDocComment = true;
×
2681
            }
2682
        } else {
2683
            returnType = codeType;
12,955✔
2684
            if (!returnType && docType) {
12,955✔
2685
                returnType = docType;
106✔
2686
                if (options.data) {
106✔
2687
                    options.data.isFromDocComment = true;
29✔
2688
                }
2689
            }
2690
        }
2691
        return returnType;
12,955✔
2692
    }
2693

2694
    /**
2695
     * Gets the type for a default value (eg. as a function param, class member or typed array)
2696
     */
2697
    public getDefaultTypeFromValueType(valueType: (BscType | BscType[])) {
2698
        if (!valueType) {
3,426✔
2699
            return undefined;
1,083✔
2700
        }
2701
        let resultType: BscType = DynamicType.instance;
2,343✔
2702
        if (Array.isArray(valueType)) {
2,343!
2703
            // passed an array opf types, potential from ArrayType.innerTypes
UNCOV
2704
            if (valueType.length > 0) {
×
2705
                //at least one, use it
UNCOV
2706
                resultType = valueType[0];
×
UNCOV
2707
                if (valueType?.length > 1) {
×
2708
                    // more than 1, find union
UNCOV
2709
                    resultType = getUniqueType(valueType, unionTypeFactory);
×
2710
                }
2711
            }
2712
        } else {
2713
            resultType = valueType;
2,343✔
2714
        }
2715
        if (!resultType.isResolvable()) {
2,343✔
2716
            if (isUnionType(resultType)) {
450✔
2717
                resultType = DynamicType.instance;
1✔
2718
            } else {
2719
                resultType = new ParamTypeFromValueReferenceType(resultType);
449✔
2720
            }
2721

2722
        } else if (isEnumMemberType(resultType)) {
1,893✔
2723
            // the type was an enum member... Try to get the parent enum type
2724
            resultType = resultType.parentEnumType ?? resultType;
24!
2725
        } else if (isUnionType(resultType)) {
1,869✔
2726
            // it was a union -- I wonder if they're resolvable now?
2727
            const moddedTypes = resultType.types.map(t => {
11✔
2728
                if (isEnumMemberType(t)) {
23✔
2729
                    // the type was an enum member... Try to get the parent enum type
2730
                    return t.parentEnumType ?? resultType;
14!
2731
                }
2732
                return t;
9✔
2733
            });
2734
            resultType = getUniqueType(moddedTypes, unionTypeFactory);
11✔
2735
        }
2736
        return resultType;
2,343✔
2737
    }
2738

2739
    /**
2740
     * Get a short name that can be used to reference the project in logs. (typically something like `prj1`, `prj8`, etc...)
2741
     */
2742
    public getProjectLogName(config: { projectNumber: number }) {
2743
        //if we have a project number, use it
2744
        if (config?.projectNumber !== undefined) {
837✔
2745
            return `prj${config.projectNumber}`;
178✔
2746
        }
2747
        //just return empty string so log functions don't crash with undefined project numbers
2748
        return '';
659✔
2749
    }
2750
}
2751

2752
/**
2753
 * 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,
2754
 * we can't use `object.tag` syntax.
2755
 */
2756
export function standardizePath(stringParts, ...expressions: any[]) {
1✔
2757
    let result: string[] = [];
27,563✔
2758
    for (let i = 0; i < stringParts?.length; i++) {
27,563!
2759
        result.push(stringParts[i], expressions[i]);
274,411✔
2760
    }
2761
    return util.standardizePath(
27,563✔
2762
        result.join('')
2763
    );
2764
}
2765

2766
/**
2767
 * An item that can be coerced into a `Range`
2768
 */
2769
export type RangeLike = { location?: Location } | Location | { range?: Range } | Range | undefined;
2770

2771
export let util = new Util();
1✔
2772
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