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

rokucommunity / brighterscript / #12930

13 Aug 2024 05:02PM UTC coverage: 86.193% (-1.7%) from 87.933%
#12930

push

web-flow
Merge 58ad447a2 into 0e968f1c3

10630 of 13125 branches covered (80.99%)

Branch coverage included in aggregate %.

6675 of 7284 new or added lines in 99 files covered. (91.64%)

84 existing lines in 18 files now uncovered.

12312 of 13492 relevant lines covered (91.25%)

26865.48 hits per line

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

85.71
/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, standardizePath as rokuDeployStandardizePath } from 'roku-deploy';
1✔
7
import type { DiagnosticRelatedInformation, Diagnostic, Position } from 'vscode-languageserver';
8
import { Location } from 'vscode-languageserver';
1✔
9
import { Range } from 'vscode-languageserver';
1✔
10
import { URI } from 'vscode-uri';
1✔
11
import * as xml2js from 'xml2js';
1✔
12
import type { BsConfig, FinalizedBsConfig } from './BsConfig';
13
import { DiagnosticMessages } from './DiagnosticMessages';
1✔
14
import type { CallableContainer, BsDiagnostic, FileReference, CallableContainerMap, CompilerPluginFactory, CompilerPlugin, ExpressionInfo, TranspileResult, TypeChainEntry, TypeChainProcessResult, GetTypeOptions } from './interfaces';
15
import { BooleanType } from './types/BooleanType';
1✔
16
import { DoubleType } from './types/DoubleType';
1✔
17
import { DynamicType } from './types/DynamicType';
1✔
18
import { FloatType } from './types/FloatType';
1✔
19
import { IntegerType } from './types/IntegerType';
1✔
20
import { LongIntegerType } from './types/LongIntegerType';
1✔
21
import { ObjectType } from './types/ObjectType';
1✔
22
import { StringType } from './types/StringType';
1✔
23
import { VoidType } from './types/VoidType';
1✔
24
import { ParseMode } from './parser/Parser';
1✔
25
import type { CallExpression, CallfuncExpression, DottedGetExpression, FunctionParameterExpression, IndexedGetExpression, LiteralExpression, NewExpression, TypeExpression, VariableExpression, XmlAttributeGetExpression } from './parser/Expression';
26
import { LogLevel, createLogger } from './logging';
1✔
27
import { isToken, type Identifier, type Locatable, type Token } from './lexer/Token';
1✔
28
import { TokenKind } from './lexer/TokenKind';
1✔
29
import { isAnyReferenceType, isBinaryExpression, isBooleanType, isBrsFile, isCallExpression, isCallableType, isCallfuncExpression, isClassType, isDottedGetExpression, isDoubleType, isDynamicType, isEnumMemberType, isExpression, isFloatType, isIndexedGetExpression, isInvalidType, isLiteralString, isLongIntegerType, isNamespaceType, isNewExpression, isNumberType, isStatement, isStringType, isTypeExpression, isTypedArrayExpression, isTypedFunctionType, isUnionType, isVariableExpression, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection';
1✔
30
import { WalkMode } from './astUtils/visitors';
1✔
31
import { SourceNode } from 'source-map';
1✔
32
import * as requireRelative from 'require-relative';
1✔
33
import type { BrsFile } from './files/BrsFile';
34
import type { XmlFile } from './files/XmlFile';
35
import type { AstNode, Expression, Statement } from './parser/AstNode';
36
import { AstNodeKind } from './parser/AstNode';
1✔
37
import type { UnresolvedSymbol } from './AstValidationSegmenter';
38
import type { SymbolTable } from './SymbolTable';
39
import { SymbolTypeFlag } from './SymbolTypeFlag';
1✔
40
import { createIdentifier, createToken } from './astUtils/creators';
1✔
41
import { MAX_RELATED_INFOS_COUNT } from './diagnosticUtils';
1✔
42
import type { BscType } from './types/BscType';
43
import { unionTypeFactory } from './types/UnionType';
1✔
44
import { ArrayType } from './types/ArrayType';
1✔
45
import { BinaryOperatorReferenceType } from './types/ReferenceType';
1✔
46
import { AssociativeArrayType } from './types/AssociativeArrayType';
1✔
47
import { ComponentType } from './types/ComponentType';
1✔
48
import { FunctionType } from './types/FunctionType';
1✔
49
import type { AssignmentStatement } from './parser/Statement';
50
import type { BscFile } from './files/BscFile';
51

52
export class Util {
1✔
53
    public clearConsole() {
54
        // process.stdout.write('\x1Bc');
55
    }
56

57
    /**
58
     * Returns the number of parent directories in the filPath
59
     */
60
    public getParentDirectoryCount(filePath: string | undefined) {
61
        if (!filePath) {
1,583!
62
            return -1;
×
63
        } else {
64
            return filePath.replace(/^pkg:/, '').split(/[\\\/]/).length - 1;
1,583✔
65
        }
66
    }
67

68
    /**
69
     * Determine if the file exists
70
     */
71
    public async pathExists(filePath: string | undefined) {
72
        if (!filePath) {
122✔
73
            return false;
1✔
74
        } else {
75
            return fsExtra.pathExists(filePath);
121✔
76
        }
77
    }
78

79
    /**
80
     * Determine if the file exists
81
     */
82
    public pathExistsSync(filePath: string | undefined) {
83
        if (!filePath) {
6,007!
84
            return false;
×
85
        } else {
86
            return fsExtra.pathExistsSync(filePath);
6,007✔
87
        }
88
    }
89

90
    /**
91
     * Determine if this path is a directory
92
     */
93
    public isDirectorySync(dirPath: string | undefined) {
94
        return dirPath !== undefined && fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
3✔
95
    }
96

97
    /**
98
     * Given a pkg path of any kind, transform it to a roku-specific pkg path (i.e. "pkg:/some/path.brs")
99
     */
100
    public sanitizePkgPath(pkgPath: string) {
101
        //convert all slashes to forwardslash
102
        pkgPath = pkgPath.replace(/[\/\\]+/g, '/');
49✔
103
        //ensure every path has the leading pkg:/
104
        return 'pkg:/' + pkgPath.replace(/^pkg:\//i, '');
49✔
105
    }
106

107
    /**
108
     * Determine if the given path starts with a protocol
109
     */
110
    public startsWithProtocol(path: string) {
111
        return !!/^[-a-z]+:\//i.exec(path);
×
112
    }
113

114
    /**
115
     * Given a pkg path of any kind, transform it to a roku-specific pkg path (i.e. "pkg:/some/path.brs")
116
     * @deprecated use `sanitizePkgPath instead. Will be removed in v1
117
     */
118
    public getRokuPkgPath(pkgPath: string) {
NEW
119
        return this.sanitizePkgPath(pkgPath);
×
120
    }
121

122
    /**
123
     * Given a path to a file/directory, replace all path separators with the current system's version.
124
     */
125
    public pathSepNormalize(filePath: string, separator?: string) {
126
        if (!filePath) {
14!
127
            return filePath;
×
128
        }
129
        separator = separator ? separator : path.sep;
14!
130
        return filePath.replace(/[\\/]+/g, separator);
14✔
131
    }
132

133
    /**
134
     * Find the path to the config file.
135
     * If the config file path doesn't exist
136
     * @param cwd the current working directory where the search for configs should begin
137
     */
138
    public getConfigFilePath(cwd?: string) {
139
        cwd = cwd ?? process.cwd();
66✔
140
        let configPath = path.join(cwd, 'bsconfig.json');
66✔
141
        //find the nearest config file path
142
        for (let i = 0; i < 100; i++) {
66✔
143
            if (this.pathExistsSync(configPath)) {
6,007✔
144
                return configPath;
6✔
145
            } else {
146
                let parentDirPath = path.dirname(path.dirname(configPath));
6,001✔
147
                configPath = path.join(parentDirPath, 'bsconfig.json');
6,001✔
148
            }
149
        }
150
    }
151

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

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

216
            let projectFileCwd = path.dirname(configFilePath);
23✔
217

218
            //`plugins` paths should be relative to the current bsconfig
219
            this.resolvePathsRelativeTo(projectConfig, 'plugins', projectFileCwd);
23✔
220

221
            //`require` paths should be relative to cwd
222
            util.resolvePathsRelativeTo(projectConfig, 'require', projectFileCwd);
23✔
223

224
            let result: BsConfig;
225
            //if the project has a base file, load it
226
            if (projectConfig && typeof projectConfig.extends === 'string') {
23✔
227
                let baseProjectConfig = this.loadConfigFile(projectConfig.extends, [...parentProjectPaths, configFilePath], projectFileCwd);
7✔
228
                //extend the base config with the current project settings
229
                result = { ...baseProjectConfig, ...projectConfig };
5✔
230
            } else {
231
                result = projectConfig;
16✔
232
                let ancestors = parentProjectPaths ? parentProjectPaths : [];
16!
233
                ancestors.push(configFilePath);
16✔
234
                (result as any)._ancestors = parentProjectPaths;
16✔
235
            }
236

237
            //make any paths in the config absolute (relative to the CURRENT config file)
238
            if (result.outFile) {
21✔
239
                result.outFile = path.resolve(projectFileCwd, result.outFile);
4✔
240
            }
241
            if (result.rootDir) {
21✔
242
                result.rootDir = path.resolve(projectFileCwd, result.rootDir);
5✔
243
            }
244
            if (result.cwd) {
21✔
245
                result.cwd = path.resolve(projectFileCwd, result.cwd);
1✔
246
            }
247
            if (result.stagingDir) {
21✔
248
                result.stagingDir = path.resolve(projectFileCwd, result.stagingDir);
2✔
249
            }
250
            return result;
21✔
251
        }
252
    }
253

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

275
    /**
276
     * Do work within the scope of a changed current working directory
277
     * @param targetCwd the cwd where the work should be performed
278
     * @param callback a function to call when the cwd has been changed to `targetCwd`
279
     */
280
    public cwdWork<T>(targetCwd: string | null | undefined, callback: () => T): T {
UNCOV
281
        let originalCwd = process.cwd();
×
UNCOV
282
        if (targetCwd) {
×
UNCOV
283
            process.chdir(targetCwd);
×
284
        }
285

286
        let result: T;
287
        let err;
288

UNCOV
289
        try {
×
UNCOV
290
            result = callback();
×
291
        } catch (e) {
292
            err = e;
×
293
        }
294

UNCOV
295
        if (targetCwd) {
×
UNCOV
296
            process.chdir(originalCwd);
×
297
        }
298

UNCOV
299
        if (err) {
×
300
            throw err;
×
301
        } else {
302
            //justification: `result` is set as long as `err` is not set and vice versa
UNCOV
303
            return result!;
×
304
        }
305
    }
306

307
    /**
308
     * Given a BsConfig object, start with defaults,
309
     * merge with bsconfig.json and the provided options.
310
     * @param config a bsconfig object to use as the baseline for the resulting config
311
     */
312
    public normalizeAndResolveConfig(config: BsConfig | undefined): FinalizedBsConfig {
313
        let result = this.normalizeConfig({});
74✔
314

315
        if (config?.noProject) {
74!
316
            return result;
1✔
317
        }
318

319
        //if no options were provided, try to find a bsconfig.json file
320
        if (!config || !config.project) {
73✔
321
            result.project = this.getConfigFilePath(config?.cwd);
61!
322
        } else {
323
            //use the config's project link
324
            result.project = config.project;
12✔
325
        }
326
        if (result.project) {
73✔
327
            let configFile = this.loadConfigFile(result.project, undefined, config?.cwd);
15!
328
            result = Object.assign(result, configFile);
13✔
329
        }
330
        //override the defaults with the specified options
331
        result = Object.assign(result, config);
71✔
332
        return result;
71✔
333
    }
334

335
    /**
336
     * Set defaults for any missing items
337
     * @param config a bsconfig object to use as the baseline for the resulting config
338
     */
339
    public normalizeConfig(config: BsConfig | undefined): FinalizedBsConfig {
340
        config = config ?? {} as BsConfig;
1,711✔
341

342
        const cwd = config.cwd ?? process.cwd();
1,711✔
343
        const rootFolderName = path.basename(cwd);
1,711✔
344
        const retainStagingDir = (config.retainStagingDir ?? config.retainStagingDir) === true ? true : false;
1,711✔
345

346
        let logLevel: LogLevel = LogLevel.log;
1,711✔
347

348
        if (typeof config.logLevel === 'string') {
1,711!
349
            logLevel = LogLevel[(config.logLevel as string).toLowerCase()] ?? LogLevel.log;
×
350
        }
351

352
        let bslibDestinationDir = config.bslibDestinationDir ?? 'source';
1,711✔
353
        if (bslibDestinationDir !== 'source') {
1,711✔
354
            // strip leading and trailing slashes
355
            bslibDestinationDir = bslibDestinationDir.replace(/^(\/*)(.*?)(\/*)$/, '$2');
4✔
356
        }
357

358
        const configWithDefaults: Omit<FinalizedBsConfig, 'rootDir'> = {
1,711✔
359
            cwd: cwd,
360
            deploy: config.deploy === true ? true : false,
1,711!
361
            //use default files array from rokuDeploy
362
            files: config.files ?? [...DefaultFiles],
5,133✔
363
            createPackage: config.createPackage === false ? false : true,
1,711✔
364
            outFile: config.outFile ?? `./out/${rootFolderName}.zip`,
5,133✔
365
            sourceMap: config.sourceMap === true,
366
            username: config.username ?? 'rokudev',
5,133✔
367
            watch: config.watch === true ? true : false,
1,711!
368
            emitFullPaths: config.emitFullPaths === true ? true : false,
1,711!
369
            retainStagingDir: retainStagingDir,
370
            copyToStaging: config.copyToStaging === false ? false : true,
1,711✔
371
            ignoreErrorCodes: config.ignoreErrorCodes ?? [],
5,133✔
372
            diagnosticSeverityOverrides: config.diagnosticSeverityOverrides ?? {},
5,133✔
373
            diagnosticFilters: config.diagnosticFilters ?? [],
5,133✔
374
            plugins: config.plugins ?? [],
5,133✔
375
            pruneEmptyCodeFiles: config.pruneEmptyCodeFiles === true ? true : false,
1,711✔
376
            autoImportComponentScript: config.autoImportComponentScript === true ? true : false,
1,711✔
377
            showDiagnosticsInConsole: config.showDiagnosticsInConsole === false ? false : true,
1,711✔
378
            sourceRoot: config.sourceRoot ? standardizePath(config.sourceRoot) : undefined,
1,711✔
379
            allowBrighterScriptInBrightScript: config.allowBrighterScriptInBrightScript === true ? true : false,
1,711!
380
            emitDefinitions: config.emitDefinitions === true ? true : false,
1,711!
381
            removeParameterTypes: config.removeParameterTypes === true ? true : false,
1,711!
382
            logLevel: logLevel,
383
            bslibDestinationDir: bslibDestinationDir,
384
            legacyCallfuncHandling: config.legacyCallfuncHandling === true ? true : false
1,711!
385
        };
386

387
        //mutate `config` in case anyone is holding a reference to the incomplete one
388
        const merged: FinalizedBsConfig = Object.assign(config, configWithDefaults);
1,711✔
389

390
        return merged;
1,711✔
391
    }
392

393
    /**
394
     * Get the root directory from options.
395
     * Falls back to options.cwd.
396
     * Falls back to process.cwd
397
     * @param options a bsconfig object
398
     */
399
    public getRootDir(options: BsConfig) {
400
        if (!options) {
1,600!
401
            throw new Error('Options is required');
×
402
        }
403
        let cwd = options.cwd;
1,600✔
404
        cwd = cwd ? cwd : process.cwd();
1,600!
405
        let rootDir = options.rootDir ? options.rootDir : cwd;
1,600✔
406

407
        rootDir = path.resolve(cwd, rootDir);
1,600✔
408

409
        return rootDir;
1,600✔
410
    }
411

412
    /**
413
     * Given a list of callables as a dictionary indexed by their full name (namespace included, transpiled to underscore-separated.
414
     */
415
    public getCallableContainersByLowerName(callables: CallableContainer[]): CallableContainerMap {
416
        //find duplicate functions
417
        const result = new Map<string, CallableContainer[]>();
1,555✔
418

419
        for (let callableContainer of callables) {
1,555✔
420
            let lowerName = callableContainer.callable.getName(ParseMode.BrightScript).toLowerCase();
122,999✔
421

422
            //create a new array for this name
423
            const list = result.get(lowerName);
122,999✔
424
            if (list) {
122,999✔
425
                list.push(callableContainer);
6,254✔
426
            } else {
427
                result.set(lowerName, [callableContainer]);
116,745✔
428
            }
429
        }
430
        return result;
1,555✔
431
    }
432

433
    /**
434
     * Split a file by newline characters (LF or CRLF)
435
     */
436
    public getLines(text: string) {
437
        return text.split(/\r?\n/);
×
438
    }
439

440
    /**
441
     * Given an absolute path to a source file, and a target path,
442
     * compute the pkg path for the target relative to the source file's location
443
     */
444
    public getPkgPathFromTarget(containingFilePathAbsolute: string, targetPath: string) {
445
        // https://regex101.com/r/w7CG2N/1
446
        const regexp = /^(?:pkg|libpkg):(\/)?/i;
628✔
447
        const [fullScheme, slash] = regexp.exec(targetPath) ?? [];
628✔
448
        //if the target starts with 'pkg:' or 'libpkg:' then it's an absolute path. Return as is
449
        if (slash) {
628✔
450
            targetPath = targetPath.substring(fullScheme.length);
418✔
451
            if (targetPath === '') {
418✔
452
                return null;
2✔
453
            } else {
454
                return path.normalize(targetPath);
416✔
455
            }
456
        }
457
        //if the path is exactly `pkg:` or `libpkg:`
458
        if (targetPath === fullScheme && !slash) {
210✔
459
            return null;
2✔
460
        }
461

462
        //remove the filename
463
        let containingFolder = path.normalize(path.dirname(containingFilePathAbsolute));
208✔
464
        //start with the containing folder, split by slash
465
        let result = containingFolder.split(path.sep);
208✔
466

467
        //split on slash
468
        let targetParts = path.normalize(targetPath).split(path.sep);
208✔
469

470
        for (let part of targetParts) {
208✔
471
            if (part === '' || part === '.') {
212✔
472
                //do nothing, it means current directory
473
                continue;
4✔
474
            }
475
            if (part === '..') {
208✔
476
                //go up one directory
477
                result.pop();
2✔
478
            } else {
479
                result.push(part);
206✔
480
            }
481
        }
482
        return result.join(path.sep);
208✔
483
    }
484

485
    /**
486
     * Compute the relative path from the source file to the target file
487
     * @param pkgSrcPath  - the absolute path to the source, where cwd is the package location
488
     * @param pkgTargetPath  - the absolute path to the target, where cwd is the package location
489
     */
490
    public getRelativePath(pkgSrcPath: string, pkgTargetPath: string) {
491
        pkgSrcPath = path.normalize(pkgSrcPath);
11✔
492
        pkgTargetPath = path.normalize(pkgTargetPath);
11✔
493

494
        //break by path separator
495
        let sourceParts = pkgSrcPath.split(path.sep);
11✔
496
        let targetParts = pkgTargetPath.split(path.sep);
11✔
497

498
        let commonParts = [] as string[];
11✔
499
        //find their common root
500
        for (let i = 0; i < targetParts.length; i++) {
11✔
501
            if (targetParts[i].toLowerCase() === sourceParts[i].toLowerCase()) {
17✔
502
                commonParts.push(targetParts[i]);
6✔
503
            } else {
504
                //we found a non-matching part...so no more commonalities past this point
505
                break;
11✔
506
            }
507
        }
508

509
        //throw out the common parts from both sets
510
        sourceParts.splice(0, commonParts.length);
11✔
511
        targetParts.splice(0, commonParts.length);
11✔
512

513
        //throw out the filename part of source
514
        sourceParts.splice(sourceParts.length - 1, 1);
11✔
515
        //start out by adding updir paths for each remaining source part
516
        let resultParts = sourceParts.map(() => '..');
11✔
517

518
        //now add every target part
519
        resultParts = [...resultParts, ...targetParts];
11✔
520
        return path.join(...resultParts);
11✔
521
    }
522

523
    public getImportPackagePath(srcPath: string, pkgTargetPath: string) {
524
        const srcExt = this.getExtension(srcPath);
6✔
525
        const lowerSrcExt = srcExt.toLowerCase();
6✔
526
        const lowerTargetExt = this.getExtension(pkgTargetPath).toLowerCase();
6✔
527
        if (lowerSrcExt === '.bs' && lowerTargetExt === '.brs') {
6✔
528
            // if source is .bs, use that as the import extenstion
529
            return pkgTargetPath.substring(0, pkgTargetPath.length - lowerTargetExt.length) + srcExt;
3✔
530
        }
531
        return pkgTargetPath;
3✔
532
    }
533

534
    /**
535
     * Walks left in a DottedGetExpression and returns a VariableExpression if found, or undefined if not found
536
     */
537
    public findBeginningVariableExpression(dottedGet: DottedGetExpression): VariableExpression | undefined {
538
        let left: any = dottedGet;
37✔
539
        while (left) {
37✔
540
            if (isVariableExpression(left)) {
69✔
541
                return left;
37✔
542
            } else if (isDottedGetExpression(left)) {
32!
543
                left = left.obj;
32✔
544
            } else {
545
                break;
×
546
            }
547
        }
548
    }
549

550
    /**
551
     * Do `a` and `b` overlap by at least one character. This returns false if they are at the edges. Here's some examples:
552
     * ```
553
     * | true | true | true | true | true | false | false | false | false |
554
     * |------|------|------|------|------|-------|-------|-------|-------|
555
     * | aa   |  aaa |  aaa | aaa  |  a   |  aa   |    aa | a     |     a |
556
     * |  bbb | bb   |  bbb |  b   | bbb  |    bb |  bb   |     b | a     |
557
     * ```
558
     */
559
    public rangesIntersect(a: Range | undefined, b: Range | undefined) {
560
        //stop if the either range is misisng
561
        if (!a || !b) {
11✔
562
            return false;
2✔
563
        }
564

565
        // Check if `a` is before `b`
566
        if (a.end.line < b.start.line || (a.end.line === b.start.line && a.end.character <= b.start.character)) {
9✔
567
            return false;
1✔
568
        }
569

570
        // Check if `b` is before `a`
571
        if (b.end.line < a.start.line || (b.end.line === a.start.line && b.end.character <= a.start.character)) {
8✔
572
            return false;
1✔
573
        }
574

575
        // These ranges must intersect
576
        return true;
7✔
577
    }
578

579
    /**
580
     * Do `a` and `b` overlap by at least one character or touch at the edges
581
     * ```
582
     * | true | true | true | true | true | true  | true  | false | false |
583
     * |------|------|------|------|------|-------|-------|-------|-------|
584
     * | aa   |  aaa |  aaa | aaa  |  a   |  aa   |    aa | a     |     a |
585
     * |  bbb | bb   |  bbb |  b   | bbb  |    bb |  bb   |     b | a     |
586
     * ```
587
     */
588
    public rangesIntersectOrTouch(a: Range | undefined, b: Range | undefined) {
589
        //stop if the either range is misisng
590
        if (!a || !b) {
27✔
591
            return false;
2✔
592
        }
593
        // Check if `a` is before `b`
594
        if (a.end.line < b.start.line || (a.end.line === b.start.line && a.end.character < b.start.character)) {
25✔
595
            return false;
2✔
596
        }
597

598
        // Check if `b` is before `a`
599
        if (b.end.line < a.start.line || (b.end.line === a.start.line && b.end.character < a.start.character)) {
23✔
600
            return false;
2✔
601
        }
602

603
        // These ranges must intersect
604
        return true;
21✔
605
    }
606

607
    /**
608
     * Test if `position` is in `range`. If the position is at the edges, will return true.
609
     * Adapted from core vscode
610
     */
611
    public rangeContains(range: Range | undefined, position: Position | undefined) {
612
        return this.comparePositionToRange(position, range) === 0;
11,578✔
613
    }
614

615
    public comparePositionToRange(position: Position | undefined, range: Range | undefined) {
616
        //stop if the either range is missng
617
        if (!position || !range) {
13,557✔
618
            return 0;
10✔
619
        }
620

621
        if (this.comparePosition(position, range.start) < 0) {
13,547✔
622
            return -1;
2,114✔
623
        }
624
        if (this.comparePosition(position, range.end) > 0) {
11,433✔
625
            return 1;
8,203✔
626
        }
627
        return 0;
3,230✔
628
    }
629

630
    public comparePosition(a: Position | undefined, b: Position) {
631
        //stop if the either position is missing
632
        if (!a || !b) {
201,955!
NEW
633
            return 0;
×
634
        }
635

636
        if (a.line < b.line || (a.line === b.line && a.character < b.character)) {
201,955✔
637
            return -1;
14,316✔
638
        }
639
        if (a.line > b.line || (a.line === b.line && a.character > b.character)) {
187,639✔
640
            return 1;
186,843✔
641
        }
642
        return 0;
796✔
643
    }
644

645
    /**
646
     * Combine all the documentation for a node - uses the AstNode's leadingTrivia property
647
     */
648
    public getNodeDocumentation(node: AstNode) {
649
        if (!node) {
935✔
650
            return '';
562✔
651
        }
652
        const nodeTrivia = node.leadingTrivia ?? [];
373!
653
        const leadingTrivia = isStatement(node)
373✔
NEW
654
            ? [...(node.annotations?.map(anno => anno.leadingTrivia ?? []).flat() ?? []), ...nodeTrivia]
×
655
            : nodeTrivia;
656
        const tokens = leadingTrivia?.filter(t => t.kind === TokenKind.Newline || t.kind === TokenKind.Comment);
798!
657
        const comments = [] as Token[];
373✔
658

659
        let newLinesInRow = 0;
373✔
660
        for (let i = tokens.length - 1; i >= 0; i--) {
373✔
661
            const token = tokens[i];
431✔
662
            //skip whitespace and newline chars
663
            if (token.kind === TokenKind.Comment) {
431✔
664
                comments.push(token);
26✔
665
                newLinesInRow = 0;
26✔
666
            } else if (token.kind === TokenKind.Newline) {
405!
667
                //skip these tokens
668
                newLinesInRow++;
405✔
669

670
                if (newLinesInRow > 1) {
405✔
671
                    // stop processing on empty line.
672
                    break;
42✔
673
                }
674
                //any other token means there are no more comments
675
            } else {
NEW
676
                break;
×
677
            }
678
        }
679
        const jsDocCommentBlockLine = /(\/\*{2,}|\*{1,}\/)/i;
373✔
680
        let usesjsDocCommentBlock = false;
373✔
681
        if (comments.length > 0) {
373✔
682
            return comments.reverse()
15✔
683
                .map(x => x.text.replace(/^('|rem)/i, '').trim())
26✔
684
                .filter(line => {
685
                    if (jsDocCommentBlockLine.exec(line)) {
26✔
686
                        usesjsDocCommentBlock = true;
4✔
687
                        return false;
4✔
688
                    }
689
                    return true;
22✔
690
                }).map(line => {
691
                    if (usesjsDocCommentBlock) {
22✔
692
                        if (line.startsWith('*')) {
2✔
693
                            //remove jsDoc leading '*'
694
                            line = line.slice(1).trim();
1✔
695
                        }
696
                    }
697
                    if (line.startsWith('@')) {
22✔
698
                        // Handle jsdoc/brightscriptdoc tags specially
699
                        // make sure they are on their own markdown line, and add italics
700
                        const firstSpaceIndex = line.indexOf(' ');
3✔
701
                        if (firstSpaceIndex === -1) {
3✔
702
                            return `\n_${line}_`;
1✔
703
                        }
704
                        const firstWord = line.substring(0, firstSpaceIndex);
2✔
705
                        return `\n_${firstWord}_ ${line.substring(firstSpaceIndex + 1)}`;
2✔
706
                    }
707
                    return line;
19✔
708
                }).join('\n');
709
        }
710
        return '';
358✔
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;
286,591✔
721
    }
722

723
    /**
724
     * Parse an xml file and get back a javascript object containing its results
725
     */
726
    public parseXml(text: string) {
727
        return new Promise<any>((resolve, reject) => {
×
728
            xml2js.parseString(text, (err, data) => {
×
729
                if (err) {
×
730
                    reject(err);
×
731
                } else {
732
                    resolve(data);
×
733
                }
734
            });
735
        });
736
    }
737

738
    public propertyCount(object: Record<string, unknown>) {
739
        let count = 0;
×
740
        for (let key in object) {
×
741
            if (object.hasOwnProperty(key)) {
×
742
                count++;
×
743
            }
744
        }
745
        return count;
×
746
    }
747

748
    public padLeft(subject: string, totalLength: number, char: string) {
749
        totalLength = totalLength > 1000 ? 1000 : totalLength;
1!
750
        while (subject.length < totalLength) {
1✔
751
            subject = char + subject;
1,000✔
752
        }
753
        return subject;
1✔
754
    }
755

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

763
    /**
764
     * Given a file path, convert it to a URI string
765
     */
766
    public pathToUri(filePath: string) {
767
        if (!filePath) {
232,691✔
768
            return filePath;
25,428✔
769
        } else if (this.isUriLike(filePath)) {
207,263✔
770
            return filePath;
187,313✔
771
        } else {
772
            return URI.file(filePath).toString();
19,950✔
773
        }
774
    }
775

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

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

796
    /**
797
     * Force the drive letter to lower case
798
     */
799
    public driveLetterToLower(fullPath: string) {
800
        if (fullPath) {
31,811✔
801
            let firstCharCode = fullPath.charCodeAt(0);
31,807✔
802
            if (
31,807✔
803
                //is upper case A-Z
804
                firstCharCode >= 65 && firstCharCode <= 90 &&
64,862✔
805
                //next char is colon
806
                fullPath[1] === ':'
807
            ) {
808
                fullPath = fullPath[0].toLowerCase() + fullPath.substring(1);
1,268✔
809
            }
810
        }
811
        return fullPath;
31,811✔
812
    }
813

814
    /**
815
     * Replace the first instance of `search` in `subject` with `replacement`
816
     */
817
    public replaceCaseInsensitive(subject: string, search: string, replacement: string) {
818
        let idx = subject.toLowerCase().indexOf(search.toLowerCase());
6,519✔
819
        if (idx > -1) {
6,519✔
820
            let result = subject.substring(0, idx) + replacement + subject.substring(idx + search.length);
2,061✔
821
            return result;
2,061✔
822
        } else {
823
            return subject;
4,458✔
824
        }
825
    }
826

827
    /**
828
     * Determine if two arrays containing primitive values are equal.
829
     * This considers order and compares by equality.
830
     */
831
    public areArraysEqual(arr1: any[], arr2: any[]) {
832
        if (arr1.length !== arr2.length) {
8✔
833
            return false;
3✔
834
        }
835
        for (let i = 0; i < arr1.length; i++) {
5✔
836
            if (arr1[i] !== arr2[i]) {
7✔
837
                return false;
3✔
838
            }
839
        }
840
        return true;
2✔
841
    }
842

843
    /**
844
     * Get the outDir from options, taking into account cwd and absolute outFile paths
845
     */
846
    public getOutDir(options: FinalizedBsConfig) {
847
        options = this.normalizeConfig(options);
2✔
848
        let cwd = path.normalize(options.cwd ? options.cwd : process.cwd());
2!
849
        if (path.isAbsolute(options.outFile)) {
2!
850
            return path.dirname(options.outFile);
×
851
        } else {
852
            return path.normalize(path.join(cwd, path.dirname(options.outFile)));
2✔
853
        }
854
    }
855

856
    /**
857
     * Get paths to all files on disc that match this project's source list
858
     */
859
    public async getFilePaths(options: FinalizedBsConfig) {
860
        let rootDir = this.getRootDir(options);
48✔
861

862
        let files = await rokuDeploy.getFilePaths(options.files, rootDir);
48✔
863
        return files;
48✔
864
    }
865

866
    /**
867
     * Given a path to a brs file, compute the path to a theoretical d.bs file.
868
     * Only `.brs` files can have typedef path, so return undefined for everything else
869
     */
870
    public getTypedefPath(brsSrcPath: string) {
871
        const typedefPath = brsSrcPath
3,337✔
872
            .replace(/\.brs$/i, '.d.bs')
873
            .toLowerCase();
874

875
        if (typedefPath.endsWith('.d.bs')) {
3,337✔
876
            return typedefPath;
1,953✔
877
        } else {
878
            return undefined;
1,384✔
879
        }
880
    }
881

882

883
    /**
884
     * Walks up the chain to find the closest bsconfig.json file
885
     */
886
    public async findClosestConfigFile(currentPath: string): Promise<string | undefined> {
887
        //make the path absolute
888
        currentPath = path.resolve(
6✔
889
            path.normalize(
890
                currentPath
891
            )
892
        );
893

894
        let previousPath: string | undefined;
895
        //using ../ on the root of the drive results in the same file path, so that's how we know we reached the top
896
        while (previousPath !== currentPath) {
6✔
897
            previousPath = currentPath;
24✔
898

899
            let bsPath = path.join(currentPath, 'bsconfig.json');
24✔
900
            let brsPath = path.join(currentPath, 'brsconfig.json');
24✔
901
            if (await this.pathExists(bsPath)) {
24✔
902
                return bsPath;
2✔
903
            } else if (await this.pathExists(brsPath)) {
22✔
904
                return brsPath;
2✔
905
            } else {
906
                //walk upwards one directory
907
                currentPath = path.resolve(path.join(currentPath, '../'));
20✔
908
            }
909
        }
910
        //got to the root path, no config file exists
911
    }
912

913
    /**
914
     * Set a timeout for the specified milliseconds, and resolve the promise once the timeout is finished.
915
     * @param milliseconds the minimum number of milliseconds to sleep for
916
     */
917
    public sleep(milliseconds: number) {
918
        return new Promise((resolve) => {
103✔
919
            //if milliseconds is 0, don't actually timeout (improves unit test throughput)
920
            if (milliseconds === 0) {
103✔
921
                process.nextTick(resolve);
89✔
922
            } else {
923
                setTimeout(resolve, milliseconds);
14✔
924
            }
925
        });
926
    }
927

928
    /**
929
     * Given an array, map and then flatten
930
     * @param array the array to flatMap over
931
     * @param callback a function that is called for every array item
932
     */
933
    public flatMap<T, R>(array: T[], callback: (arg: T) => R[]): R[] {
934
        return Array.prototype.concat.apply([], array.map(callback));
16✔
935
    }
936

937
    /**
938
     * Determines if the position is greater than the range. This means
939
     * the position does not touch the range, and has a position greater than the end
940
     * of the range. A position that touches the last line/char of a range is considered greater
941
     * than the range, because the `range.end` is EXclusive
942
     */
943
    public positionIsGreaterThanRange(position: Position, range: Range) {
944

945
        //if the position is a higher line than the range
946
        if (position.line > range.end.line) {
1,166✔
947
            return true;
1,079✔
948
        } else if (position.line < range.end.line) {
87!
UNCOV
949
            return false;
×
950
        }
951
        //they are on the same line
952

953
        //if the position's char is greater than or equal to the range's
954
        if (position.character >= range.end.character) {
87!
955
            return true;
87✔
956
        } else {
UNCOV
957
            return false;
×
958
        }
959
    }
960

961
    /**
962
     * Get a range back from an object that contains (or is) a range
963
     */
964
    public extractRange(rangeIsh: RangeLike): Range | undefined {
965
        if (!rangeIsh) {
9,090✔
966
            return undefined;
29✔
967
        } else if ('location' in rangeIsh) {
9,061✔
968
            return rangeIsh.location?.range;
6,278✔
969
        } else if ('range' in rangeIsh) {
2,783!
970
            return rangeIsh.range;
2,783✔
NEW
971
        } else if (Range.is(rangeIsh)) {
×
NEW
972
            return rangeIsh;
×
973
        } else {
NEW
974
            return undefined;
×
975
        }
976
    }
977

978

979
    /**
980
     * Get a location object back by extracting location information from other objects that contain location
981
     */
982
    public getRange(startObj: | { range: Range }, endObj: { range: Range }): Range {
UNCOV
983
        if (!startObj?.range || !endObj?.range) {
×
UNCOV
984
            return undefined;
×
985
        }
UNCOV
986
        return util.createRangeFromPositions(startObj.range?.start, endObj.range?.end);
×
987
    }
988

989
    /**
990
     * If the two items both start on the same line
991
     */
992
    public sameStartLine(first: { range: Range }, second: { range: Range }) {
993
        if (first && second && first.range.start.line === second.range.start.line) {
×
994
            return true;
×
995
        } else {
996
            return false;
×
997
        }
998
    }
999

1000
    /**
1001
     * If the two items have lines that touch
1002
     */
1003
    public linesTouch(first: RangeLike, second: RangeLike) {
1004
        const firstRange = this.extractRange(first);
1,420✔
1005
        const secondRange = this.extractRange(second);
1,420✔
1006
        if (firstRange && secondRange && (
1,420✔
1007
            firstRange.start.line === secondRange.start.line ||
1008
            firstRange.start.line === secondRange.end.line ||
1009
            firstRange.end.line === secondRange.start.line ||
1010
            firstRange.end.line === secondRange.end.line
1011
        )) {
1012
            return true;
91✔
1013
        } else {
1014
            return false;
1,329✔
1015
        }
1016
    }
1017

1018
    /**
1019
     * Given text with (or without) dots separating text, get the rightmost word.
1020
     * (i.e. given "A.B.C", returns "C". or "B" returns "B because there's no dot)
1021
     */
1022
    public getTextAfterFinalDot(name: string) {
UNCOV
1023
        if (name) {
×
UNCOV
1024
            let parts = name.split('.');
×
UNCOV
1025
            if (parts.length > 0) {
×
UNCOV
1026
                return parts[parts.length - 1];
×
1027
            }
1028
        }
1029
    }
1030

1031
    /**
1032
     * Find a script import that the current position touches, or undefined if not found
1033
     */
1034
    public getScriptImportAtPosition(scriptImports: FileReference[], position: Position): FileReference | undefined {
1035
        let scriptImport = scriptImports.find((x) => {
117✔
1036
            return x.filePathRange &&
5✔
1037
                x.filePathRange.start.line === position.line &&
1038
                //column between start and end
1039
                position.character >= x.filePathRange.start.character &&
1040
                position.character <= x.filePathRange.end.character;
1041
        });
1042
        return scriptImport;
117✔
1043
    }
1044

1045
    /**
1046
     * Given the class name text, return a namespace-prefixed name.
1047
     * If the name already has a period in it, or the namespaceName was not provided, return the class name as is.
1048
     * If the name does not have a period, and a namespaceName was provided, return the class name prepended by the namespace name.
1049
     * If no namespace is provided, return the `className` unchanged.
1050
     */
1051
    public getFullyQualifiedClassName(className: string, namespaceName?: string) {
1052
        if (className?.includes('.') === false && namespaceName) {
3,705✔
1053
            return `${namespaceName}.${className}`;
153✔
1054
        } else {
1055
            return className;
3,552✔
1056
        }
1057
    }
1058

1059
    public splitIntoLines(string: string) {
1060
        return string.split(/\r?\n/g);
169✔
1061
    }
1062

1063
    public getTextForRange(string: string | string[], range: Range): string {
1064
        let lines: string[];
1065
        if (Array.isArray(string)) {
171✔
1066
            lines = string;
170✔
1067
        } else {
1068
            lines = this.splitIntoLines(string);
1✔
1069
        }
1070

1071
        const start = range.start;
171✔
1072
        const end = range.end;
171✔
1073

1074
        let endCharacter = end.character;
171✔
1075
        // If lines are the same we need to subtract out our new starting position to make it work correctly
1076
        if (start.line === end.line) {
171✔
1077
            endCharacter -= start.character;
159✔
1078
        }
1079

1080
        let rangeLines = [lines[start.line].substring(start.character)];
171✔
1081
        for (let i = start.line + 1; i <= end.line; i++) {
171✔
1082
            rangeLines.push(lines[i]);
12✔
1083
        }
1084
        const lastLine = rangeLines.pop();
171✔
1085
        if (lastLine !== undefined) {
171!
1086
            rangeLines.push(lastLine.substring(0, endCharacter));
171✔
1087
        }
1088
        return rangeLines.join('\n');
171✔
1089
    }
1090

1091
    /**
1092
     * Helper for creating `Location` objects. Prefer using this function because vscode-languageserver's `Location.create()` is significantly slower at scale
1093
     */
1094
    public createLocationFromRange(uri: string, range: Range): Location {
1095
        return {
7,723✔
1096
            uri: util.pathToUri(uri),
1097
            range: range
1098
        };
1099
    }
1100

1101
    /**
1102
     * Helper for creating `Location` objects from a file and range
1103
     */
1104
    public createLocationFromFileRange(file: BscFile, range: Range): Location {
1105
        return this.createLocationFromRange(this.pathToUri(file?.srcPath), range);
367!
1106
    }
1107

1108
    /**
1109
     * 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
1110
     */
1111
    public createLocation(startLine: number, startCharacter: number, endLine: number, endCharacter: number, uri?: string): Location {
1112
        return {
216,759✔
1113
            uri: util.pathToUri(uri),
1114
            range: {
1115
                start: {
1116
                    line: startLine,
1117
                    character: startCharacter
1118
                },
1119
                end: {
1120
                    line: endLine,
1121
                    character: endCharacter
1122
                }
1123
            }
1124
        };
1125
    }
1126

1127
    /**
1128
     * Helper for creating `Range` objects. Prefer using this function because vscode-languageserver's `Range.create()` is significantly slower.
1129
     */
1130
    public createRange(startLine: number, startCharacter: number, endLine: number, endCharacter: number): Range {
1131
        return {
7,772✔
1132
            start: {
1133
                line: startLine,
1134
                character: startCharacter
1135
            },
1136
            end: {
1137
                line: endLine,
1138
                character: endCharacter
1139
            }
1140
        };
1141
    }
1142

1143
    /**
1144
     * Create a `Range` from two `Position`s
1145
     */
1146
    public createRangeFromPositions(startPosition: Position, endPosition: Position): Range | undefined {
1147
        startPosition = startPosition ?? endPosition;
171✔
1148
        endPosition = endPosition ?? startPosition;
171✔
1149
        if (!startPosition && !endPosition) {
171!
NEW
1150
            return undefined;
×
1151
        }
1152
        return this.createRange(startPosition.line, startPosition.character, endPosition.line, endPosition.character);
171✔
1153
    }
1154

1155
    /**
1156
     *  Gets the bounding range of a bunch of ranges or objects that have ranges
1157
     *  TODO: this does a full iteration of the args. If the args were guaranteed to be in range order, we could optimize this
1158
     */
1159
    public createBoundingLocation(...locatables: Array<{ location?: Location } | Location | { range?: Range } | Range | undefined>): Location | undefined {
1160
        let uri: string | undefined;
1161
        let startPosition: Position | undefined;
1162
        let endPosition: Position | undefined;
1163

1164
        for (let locatable of locatables) {
42,629✔
1165
            let range: Range;
1166
            if (!locatable) {
172,736✔
1167
                continue;
46,418✔
1168
            } else if ('location' in locatable) {
126,318✔
1169
                range = locatable.location?.range;
121,333✔
1170
                if (!uri) {
121,333✔
1171
                    uri = locatable.location?.uri;
47,151✔
1172
                }
1173
            } else if (Location.is(locatable)) {
4,985✔
1174
                range = locatable.range;
4,977✔
1175
                if (!uri) {
4,977✔
1176
                    uri = locatable.uri;
4,396✔
1177
                }
1178
            } else if ('range' in locatable) {
8!
NEW
1179
                range = locatable.range;
×
1180
            } else {
1181
                range = locatable as Range;
8✔
1182
            }
1183

1184
            //skip undefined locations or locations without a range
1185
            if (!range) {
126,318✔
1186
                continue;
3,530✔
1187
            }
1188

1189
            if (!startPosition) {
122,788✔
1190
                startPosition = range.start;
41,430✔
1191
            } else if (this.comparePosition(range.start, startPosition) < 0) {
81,358✔
1192
                startPosition = range.start;
824✔
1193
            }
1194
            if (!endPosition) {
122,788✔
1195
                endPosition = range.end;
41,430✔
1196
            } else if (this.comparePosition(range.end, endPosition) > 0) {
81,358✔
1197
                endPosition = range.end;
76,764✔
1198
            }
1199
        }
1200
        if (startPosition && endPosition) {
42,629✔
1201
            return util.createLocation(startPosition.line, startPosition.character, endPosition.line, endPosition.character, uri);
41,430✔
1202
        } else {
1203
            return undefined;
1,199✔
1204
        }
1205
    }
1206

1207
    /**
1208
     *  Gets the bounding range of a bunch of ranges or objects that have ranges
1209
     *  TODO: this does a full iteration of the args. If the args were guaranteed to be in range order, we could optimize this
1210
     */
1211
    public createBoundingRange(...locatables: Array<RangeLike>): Range | undefined {
1212
        return this.createBoundingLocation(...locatables)?.range;
503✔
1213
    }
1214

1215
    /**
1216
     * Gets the bounding range of an object that contains a bunch of tokens
1217
     * @param tokens Object with tokens in it
1218
     * @returns Range containing all the tokens
1219
     */
1220
    public createBoundingLocationFromTokens(tokens: Record<string, { location?: Location }>): Location | undefined {
1221
        let uri: string;
1222
        let startPosition: Position | undefined;
1223
        let endPosition: Position | undefined;
1224
        for (let key in tokens) {
4,782✔
1225
            let token = tokens?.[key];
17,019!
1226
            let locatableRange = token?.location?.range;
17,019✔
1227
            if (!locatableRange) {
17,019✔
1228
                continue;
5,269✔
1229
            }
1230

1231
            if (!startPosition) {
11,750✔
1232
                startPosition = locatableRange.start;
4,626✔
1233
            } else if (this.comparePosition(locatableRange.start, startPosition) < 0) {
7,124✔
1234
                startPosition = locatableRange.start;
1,979✔
1235
            }
1236
            if (!endPosition) {
11,750✔
1237
                endPosition = locatableRange.end;
4,626✔
1238
            } else if (this.comparePosition(locatableRange.end, endPosition) > 0) {
7,124✔
1239
                endPosition = locatableRange.end;
5,033✔
1240
            }
1241
            if (!uri) {
11,750✔
1242
                uri = token.location.uri;
5,671✔
1243
            }
1244
        }
1245
        if (startPosition && endPosition) {
4,782✔
1246
            return this.createLocation(startPosition.line, startPosition.character, endPosition.line, endPosition.character, uri);
4,626✔
1247
        } else {
1248
            return undefined;
156✔
1249
        }
1250
    }
1251

1252
    /**
1253
     * Create a `Position` object. Prefer this over `Position.create` for performance reasons.
1254
     */
1255
    public createPosition(line: number, character: number) {
1256
        return {
367✔
1257
            line: line,
1258
            character: character
1259
        };
1260
    }
1261

1262
    /**
1263
     * Convert a list of tokens into a string, including their leading whitespace
1264
     */
1265
    public tokensToString(tokens: Token[]) {
1266
        let result = '';
1✔
1267
        //skip iterating the final token
1268
        for (let token of tokens) {
1✔
1269
            result += token.leadingWhitespace + token.text;
16✔
1270
        }
1271
        return result;
1✔
1272
    }
1273

1274
    /**
1275
     * Convert a token into a BscType
1276
     */
1277
    public tokenToBscType(token: Token) {
1278
        // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
1279
        switch (token.kind) {
1,589,183✔
1280
            case TokenKind.Boolean:
1,592,314✔
1281
                return new BooleanType(token.text);
101✔
1282
            case TokenKind.True:
1283
            case TokenKind.False:
1284
                return BooleanType.instance;
157✔
1285
            case TokenKind.Double:
1286
                return new DoubleType(token.text);
74✔
1287
            case TokenKind.DoubleLiteral:
1288
                return DoubleType.instance;
8✔
1289
            case TokenKind.Dynamic:
1290
                return new DynamicType(token.text);
95✔
1291
            case TokenKind.Float:
1292
                return new FloatType(token.text);
293✔
1293
            case TokenKind.FloatLiteral:
1294
                return FloatType.instance;
111✔
1295
            case TokenKind.Function:
1296
                return new FunctionType(token.text);
135✔
1297
            case TokenKind.Integer:
1298
                return new IntegerType(token.text);
1,012✔
1299
            case TokenKind.IntegerLiteral:
1300
                return IntegerType.instance;
1,808✔
1301
            case TokenKind.Invalid:
1302
                return DynamicType.instance; // TODO: use InvalidType better new InvalidType(token.text);
78✔
1303
            case TokenKind.LongInteger:
1304
                return new LongIntegerType(token.text);
46✔
1305
            case TokenKind.LongIntegerLiteral:
1306
                return LongIntegerType.instance;
3✔
1307
            case TokenKind.Object:
1308
                return new ObjectType(token.text);
307✔
1309
            case TokenKind.String:
1310
                return new StringType(token.text);
1,704✔
1311
            case TokenKind.StringLiteral:
1312
            case TokenKind.TemplateStringExpressionBegin:
1313
            case TokenKind.TemplateStringExpressionEnd:
1314
            case TokenKind.TemplateStringQuasi:
1315
                return StringType.instance;
1,009✔
1316
            case TokenKind.Void:
1317
                return new VoidType(token.text);
24✔
1318
            case TokenKind.Identifier:
1319
                switch (token.text.toLowerCase()) {
1,582,202✔
1320
                    case 'boolean':
853,648!
1321
                        return new BooleanType(token.text);
197,110✔
1322
                    case 'double':
1323
                        return new DoubleType(token.text);
4✔
1324
                    case 'dynamic':
1325
                        return new DynamicType(token.text);
4✔
1326
                    case 'float':
1327
                        return new FloatType(token.text);
197,112✔
1328
                    case 'function':
NEW
1329
                        return new FunctionType(token.text);
×
1330
                    case 'integer':
1331
                        return new IntegerType(token.text);
161,412✔
1332
                    case 'invalid':
NEW
1333
                        return DynamicType.instance; // TODO: use InvalidType better new InvalidType(token.text);
×
1334
                    case 'longinteger':
1335
                        return new LongIntegerType(token.text);
4✔
1336
                    case 'object':
1337
                        return new ObjectType(token.text);
4✔
1338
                    case 'string':
1339
                        return new StringType(token.text);
297,994✔
1340
                    case 'void':
1341
                        return new VoidType(token.text);
4✔
1342
                }
1343
        }
1344
    }
1345

1346
    /**
1347
     * Deciphers the correct types for fields based on docs
1348
     * https://developer.roku.com/en-ca/docs/references/scenegraph/xml-elements/interface.md
1349
     * @param typeDescriptor the type descriptor from the docs
1350
     * @returns {BscType} the known type, or dynamic
1351
     */
1352
    public getNodeFieldType(typeDescriptor: string, lookupTable?: SymbolTable): BscType {
1353
        let typeDescriptorLower = typeDescriptor.toLowerCase().trim().replace(/\*/g, '');
1,572,215✔
1354

1355
        if (typeDescriptorLower.startsWith('as ')) {
1,572,215✔
1356
            typeDescriptorLower = typeDescriptorLower.substring(3).trim();
6,208✔
1357
        }
1358
        const nodeFilter = (new RegExp(/^\[?(.* node)/, 'i')).exec(typeDescriptorLower);
1,572,215✔
1359
        if (nodeFilter?.[1]) {
1,572,215✔
1360
            typeDescriptorLower = nodeFilter[1].trim();
32,592✔
1361
        }
1362
        const parensFilter = (new RegExp(/(.*)\(.*\)/, 'gi')).exec(typeDescriptorLower);
1,572,215✔
1363
        if (parensFilter?.[1]) {
1,572,215✔
1364
            typeDescriptorLower = parensFilter[1].trim();
3,104✔
1365
        }
1366

1367
        const bscType = this.tokenToBscType(createToken(TokenKind.Identifier, typeDescriptorLower));
1,572,215✔
1368
        if (bscType) {
1,572,215✔
1369
            return bscType;
853,612✔
1370
        }
1371

1372
        function getRect2dType() {
1373
            const rect2dType = new AssociativeArrayType();
4,660✔
1374
            rect2dType.addMember('height', {}, FloatType.instance, SymbolTypeFlag.runtime);
4,660✔
1375
            rect2dType.addMember('width', {}, FloatType.instance, SymbolTypeFlag.runtime);
4,660✔
1376
            rect2dType.addMember('x', {}, FloatType.instance, SymbolTypeFlag.runtime);
4,660✔
1377
            rect2dType.addMember('y', {}, FloatType.instance, SymbolTypeFlag.runtime);
4,660✔
1378
            return rect2dType;
4,660✔
1379
        }
1380

1381
        function getColorType() {
1382
            return unionTypeFactory([IntegerType.instance, StringType.instance]);
96,228✔
1383
        }
1384

1385
        //check for uniontypes
1386
        const multipleTypes = typeDescriptorLower.split(' or ').map(s => s.trim());
721,707✔
1387
        if (multipleTypes.length > 1) {
718,603✔
1388
            const individualTypes = multipleTypes.map(t => this.getNodeFieldType(t, lookupTable));
6,208✔
1389
            return unionTypeFactory(individualTypes);
3,104✔
1390
        }
1391

1392
        const typeIsArray = typeDescriptorLower.startsWith('array of ') || typeDescriptorLower.startsWith('roarray of ');
715,499✔
1393

1394
        if (typeIsArray) {
715,499✔
1395
            const ofSearch = ' of ';
90,016✔
1396
            const arrayPrefixLength = typeDescriptorLower.indexOf(ofSearch) + ofSearch.length;
90,016✔
1397
            let arrayOfTypeName = typeDescriptorLower.substring(arrayPrefixLength); //cut off beginnin, eg. 'array of' or 'roarray of'
90,016✔
1398
            if (arrayOfTypeName.endsWith('s')) {
90,016✔
1399
                // remove "s" in "floats", etc.
1400
                arrayOfTypeName = arrayOfTypeName.substring(0, arrayOfTypeName.length - 1);
66,736✔
1401
            }
1402
            if (arrayOfTypeName.endsWith('\'')) {
90,016✔
1403
                // remove "'" in "float's", etc.
1404
                arrayOfTypeName = arrayOfTypeName.substring(0, arrayOfTypeName.length - 1);
6,208✔
1405
            }
1406
            if (arrayOfTypeName === 'rectangle') {
90,016✔
1407
                arrayOfTypeName = 'rect2d';
1,552✔
1408
            }
1409
            let arrayType = this.getNodeFieldType(arrayOfTypeName, lookupTable);
90,016✔
1410
            return new ArrayType(arrayType);
90,016✔
1411
        } else if (typeDescriptorLower.startsWith('option ')) {
625,483✔
1412
            const actualTypeName = typeDescriptorLower.substring('option '.length); //cut off beginning 'option '
31,040✔
1413
            return this.getNodeFieldType(actualTypeName, lookupTable);
31,040✔
1414
        } else if (typeDescriptorLower.startsWith('value ')) {
594,443✔
1415
            const actualTypeName = typeDescriptorLower.substring('value '.length); //cut off beginning 'value '
12,416✔
1416
            return this.getNodeFieldType(actualTypeName, lookupTable);
12,416✔
1417
        } else if (typeDescriptorLower === 'n/a') {
582,027✔
1418
            return DynamicType.instance;
3,104✔
1419
        } else if (typeDescriptorLower === 'uri') {
578,923✔
1420
            return StringType.instance;
111,749✔
1421
        } else if (typeDescriptorLower === 'color') {
467,174✔
1422
            return getColorType();
96,227✔
1423
        } else if (typeDescriptorLower === 'vector2d' || typeDescriptorLower === 'floatarray') {
370,947✔
1424
            return new ArrayType(FloatType.instance);
35,697✔
1425
        } else if (typeDescriptorLower === 'vector2darray') {
335,250!
NEW
1426
            return new ArrayType(new ArrayType(FloatType.instance));
×
1427
        } else if (typeDescriptorLower === 'intarray') {
335,250✔
1428
            return new ArrayType(IntegerType.instance);
1✔
1429
        } else if (typeDescriptorLower === 'colorarray') {
335,249✔
1430
            return new ArrayType(getColorType());
1✔
1431
        } else if (typeDescriptorLower === 'boolarray') {
335,248!
NEW
1432
            return new ArrayType(BooleanType.instance);
×
1433
        } else if (typeDescriptorLower === 'stringarray' || typeDescriptorLower === 'strarray') {
335,248✔
1434
            return new ArrayType(StringType.instance);
1✔
1435
        } else if (typeDescriptorLower === 'int') {
335,247✔
1436
            return IntegerType.instance;
6,208✔
1437
        } else if (typeDescriptorLower === 'time') {
329,039✔
1438
            return DoubleType.instance;
29,489✔
1439
        } else if (typeDescriptorLower === 'str') {
299,550!
NEW
1440
            return StringType.instance;
×
1441
        } else if (typeDescriptorLower === 'bool') {
299,550✔
1442
            return BooleanType.instance;
1,552✔
1443
        } else if (typeDescriptorLower === 'array' || typeDescriptorLower === 'roarray') {
297,998✔
1444
            return new ArrayType();
12,417✔
1445
        } else if (typeDescriptorLower === 'assocarray' ||
285,581✔
1446
            typeDescriptorLower === 'associative array' ||
1447
            typeDescriptorLower === 'associativearray' ||
1448
            typeDescriptorLower === 'roassociativearray' ||
1449
            typeDescriptorLower.startsWith('associative array of') ||
1450
            typeDescriptorLower.startsWith('associativearray of') ||
1451
            typeDescriptorLower.startsWith('roassociativearray of')
1452
        ) {
1453
            return new AssociativeArrayType();
54,321✔
1454
        } else if (typeDescriptorLower === 'node') {
231,260✔
1455
            return ComponentType.instance;
13,969✔
1456
        } else if (typeDescriptorLower === 'nodearray') {
217,291✔
1457
            return new ArrayType(ComponentType.instance);
1✔
1458
        } else if (typeDescriptorLower === 'rect2d') {
217,290✔
1459
            return getRect2dType();
4,658✔
1460
        } else if (typeDescriptorLower === 'rect2darray') {
212,632✔
1461
            return new ArrayType(getRect2dType());
2✔
1462
        } else if (typeDescriptorLower === 'font') {
212,630✔
1463
            return this.getNodeFieldType('roSGNodeFont', lookupTable);
34,146✔
1464
        } else if (typeDescriptorLower === 'contentnode') {
178,484✔
1465
            return this.getNodeFieldType('roSGNodeContentNode', lookupTable);
31,040✔
1466
        } else if (typeDescriptorLower.endsWith(' node')) {
147,444✔
1467
            return this.getNodeFieldType('roSgNode' + typeDescriptorLower.substring(0, typeDescriptorLower.length - 5), lookupTable);
31,040✔
1468
        } else if (lookupTable) {
116,404!
1469
            //try doing a lookup
1470
            return lookupTable.getSymbolType(typeDescriptorLower, {
116,404✔
1471
                flags: SymbolTypeFlag.typetime,
1472
                fullName: typeDescriptor,
1473
                tableProvider: () => lookupTable
2✔
1474
            });
1475
        }
1476

NEW
1477
        return DynamicType.instance;
×
1478
    }
1479

1480
    /**
1481
     * Return the type of the result of a binary operator
1482
     * Note: compound assignments (eg. +=) internally use a binary expression, so that's why TokenKind.PlusEqual, etc. are here too
1483
     */
1484
    public binaryOperatorResultType(leftType: BscType, operator: Token, rightType: BscType): BscType {
1485
        if ((isAnyReferenceType(leftType) && !leftType.isResolvable()) ||
505✔
1486
            (isAnyReferenceType(rightType) && !rightType.isResolvable())) {
1487
            return new BinaryOperatorReferenceType(leftType, operator, rightType, (lhs, op, rhs) => {
29✔
1488
                return this.binaryOperatorResultType(lhs, op, rhs);
2✔
1489
            });
1490
        }
1491
        if (isEnumMemberType(leftType)) {
476✔
1492
            leftType = leftType.underlyingType;
9✔
1493
        }
1494
        if (isEnumMemberType(rightType)) {
476✔
1495
            rightType = rightType.underlyingType;
8✔
1496
        }
1497
        let hasDouble = isDoubleType(leftType) || isDoubleType(rightType);
476✔
1498
        let hasFloat = isFloatType(leftType) || isFloatType(rightType);
476✔
1499
        let hasLongInteger = isLongIntegerType(leftType) || isLongIntegerType(rightType);
476✔
1500
        let hasInvalid = isInvalidType(leftType) || isInvalidType(rightType);
476✔
1501
        let hasDynamic = isDynamicType(leftType) || isDynamicType(rightType);
476✔
1502
        let bothNumbers = isNumberType(leftType) && isNumberType(rightType);
476✔
1503
        let bothStrings = isStringType(leftType) && isStringType(rightType);
476✔
1504
        let eitherBooleanOrNum = (isNumberType(leftType) || isBooleanType(leftType)) && (isNumberType(rightType) || isBooleanType(rightType));
476✔
1505

1506
        // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
1507
        switch (operator.kind) {
476✔
1508
            // Math operators
1509
            case TokenKind.Plus:
1,452✔
1510
            case TokenKind.PlusEqual:
1511
                if (bothStrings) {
209✔
1512
                    // "string" + "string" is the only binary expression allowed with strings
1513
                    return StringType.instance;
117✔
1514
                }
1515
            // eslint-disable-next-line no-fallthrough
1516
            case TokenKind.Minus:
1517
            case TokenKind.MinusEqual:
1518
            case TokenKind.Star:
1519
            case TokenKind.StarEqual:
1520
            case TokenKind.Mod:
1521
                if (bothNumbers) {
154✔
1522
                    if (hasDouble) {
125✔
1523
                        return DoubleType.instance;
5✔
1524
                    } else if (hasFloat) {
120✔
1525
                        return FloatType.instance;
23✔
1526

1527
                    } else if (hasLongInteger) {
97✔
1528
                        return LongIntegerType.instance;
4✔
1529
                    }
1530
                    return IntegerType.instance;
93✔
1531
                }
1532
                break;
29✔
1533
            case TokenKind.Forwardslash:
1534
            case TokenKind.ForwardslashEqual:
1535
                if (bothNumbers) {
10✔
1536
                    if (hasDouble) {
8✔
1537
                        return DoubleType.instance;
1✔
1538
                    } else if (hasFloat) {
7✔
1539
                        return FloatType.instance;
1✔
1540

1541
                    } else if (hasLongInteger) {
6✔
1542
                        return LongIntegerType.instance;
1✔
1543
                    }
1544
                    return FloatType.instance;
5✔
1545
                }
1546
                break;
2✔
1547
            case TokenKind.Backslash:
1548
            case TokenKind.BackslashEqual:
1549
                if (bothNumbers) {
6✔
1550
                    if (hasLongInteger) {
4!
NEW
1551
                        return LongIntegerType.instance;
×
1552
                    }
1553
                    return IntegerType.instance;
4✔
1554
                }
1555
                break;
2✔
1556
            case TokenKind.Caret:
1557
                if (bothNumbers) {
16✔
1558
                    if (hasDouble || hasLongInteger) {
14✔
1559
                        return DoubleType.instance;
2✔
1560
                    } else if (hasFloat) {
12✔
1561
                        return FloatType.instance;
1✔
1562
                    }
1563
                    return IntegerType.instance;
11✔
1564
                }
1565
                break;
2✔
1566
            // Bitshift operators
1567
            case TokenKind.LeftShift:
1568
            case TokenKind.LeftShiftEqual:
1569
            case TokenKind.RightShift:
1570
            case TokenKind.RightShiftEqual:
1571
                if (bothNumbers) {
18✔
1572
                    if (hasLongInteger) {
14✔
1573
                        return LongIntegerType.instance;
2✔
1574
                    }
1575
                    // Bitshifts are allowed with non-integer numerics
1576
                    // but will always truncate to ints
1577
                    return IntegerType.instance;
12✔
1578
                }
1579
                break;
4✔
1580
            // Comparison operators
1581
            // All comparison operators result in boolean
1582
            case TokenKind.Equal:
1583
            case TokenKind.LessGreater:
1584
                // = and <> can accept invalid / dynamic
1585
                if (hasDynamic || hasInvalid || bothStrings || eitherBooleanOrNum) {
80✔
1586
                    return BooleanType.instance;
77✔
1587
                }
1588
                break;
3✔
1589
            case TokenKind.Greater:
1590
            case TokenKind.Less:
1591
            case TokenKind.GreaterEqual:
1592
            case TokenKind.LessEqual:
1593
                if (bothStrings || bothNumbers) {
23✔
1594
                    return BooleanType.instance;
11✔
1595
                }
1596
                break;
12✔
1597
            // Logical or bitwise operators
1598
            case TokenKind.Or:
1599
            case TokenKind.And:
1600
                if (bothNumbers) {
52✔
1601
                    // "and"/"or" represent bitwise operators
1602
                    if (hasLongInteger && !hasDouble && !hasFloat) {
12✔
1603
                        // 2 long ints or long int and int
1604
                        return LongIntegerType.instance;
1✔
1605
                    }
1606
                    return IntegerType.instance;
11✔
1607
                } else if (eitherBooleanOrNum) {
40✔
1608
                    // "and"/"or" represent logical operators
1609
                    return BooleanType.instance;
33✔
1610
                }
1611
                break;
7✔
1612
        }
1613
        return DynamicType.instance;
61✔
1614
    }
1615

1616
    /**
1617
     * Return the type of the result of a binary operator
1618
     */
1619
    public unaryOperatorResultType(operator: Token, exprType: BscType): BscType {
1620
        // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
1621
        switch (operator.kind) {
86✔
1622
            // Math operators
1623
            case TokenKind.Minus:
82✔
1624
                if (isNumberType(exprType)) {
59✔
1625
                    // a negative number will be the same type, eg, double->double, int->int, etc.
1626
                    return exprType;
49✔
1627
                }
1628
                break;
10✔
1629
            case TokenKind.Not:
1630
                if (isBooleanType(exprType)) {
23✔
1631
                    return BooleanType.instance;
10✔
1632
                } else if (isNumberType(exprType)) {
13✔
1633
                    //numbers can be "notted"
1634
                    // by default they go to ints, except longints, which stay that way
1635
                    if (isLongIntegerType(exprType)) {
10✔
1636
                        return LongIntegerType.instance;
1✔
1637
                    }
1638
                    return IntegerType.instance;
9✔
1639
                }
1640
                break;
3✔
1641
        }
1642
        return DynamicType.instance;
17✔
1643
    }
1644

1645
    /**
1646
     * Get the extension for the given file path. Basically the part after the final dot, except for
1647
     * `d.bs` which is treated as single extension
1648
     * @returns the file extension (i.e. ".d.bs", ".bs", ".brs", ".xml", ".jpg", etc...)
1649
     */
1650
    public getExtension(filePath: string) {
1651
        filePath = filePath.toLowerCase();
2,483✔
1652
        if (filePath.endsWith('.d.bs')) {
2,483✔
1653
            return '.d.bs';
33✔
1654
        } else {
1655
            return path.extname(filePath).toLowerCase();
2,450✔
1656
        }
1657
    }
1658

1659
    /**
1660
     * Load and return the list of plugins
1661
     */
1662
    public loadPlugins(cwd: string, pathOrModules: string[], onError?: (pathOrModule: string, err: Error) => void): CompilerPlugin[] {
1663
        const logger = createLogger();
53✔
1664
        return pathOrModules.reduce<CompilerPlugin[]>((acc, pathOrModule) => {
53✔
1665
            if (typeof pathOrModule === 'string') {
6!
1666
                try {
6✔
1667
                    const loaded = requireRelative(pathOrModule, cwd);
6✔
1668
                    const theExport: CompilerPlugin | CompilerPluginFactory = loaded.default ? loaded.default : loaded;
6✔
1669

1670
                    let plugin: CompilerPlugin | undefined;
1671

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

1677
                        // the official plugin format is a factory function that returns a new instance of a plugin.
1678
                    } else if (typeof theExport === 'function') {
4!
1679
                        plugin = theExport();
4✔
1680
                    } else {
1681
                        //this should never happen; somehow an invalid plugin has made it into here
1682
                        throw new Error(`TILT: Encountered an invalid plugin: ${String(plugin)}`);
×
1683
                    }
1684

1685
                    if (!plugin.name) {
6!
1686
                        plugin.name = pathOrModule;
×
1687
                    }
1688
                    acc.push(plugin);
6✔
1689
                } catch (err: any) {
1690
                    if (onError) {
×
1691
                        onError(pathOrModule, err);
×
1692
                    } else {
1693
                        throw err;
×
1694
                    }
1695
                }
1696
            }
1697
            return acc;
6✔
1698
        }, []);
1699
    }
1700

1701
    /**
1702
     * Gathers expressions, variables, and unique names from an expression.
1703
     * This is mostly used for the ternary expression
1704
     */
1705
    public getExpressionInfo(expression: Expression, file: BrsFile): ExpressionInfo {
1706
        const expressions = [expression];
78✔
1707
        const variableExpressions = [] as VariableExpression[];
78✔
1708
        const uniqueVarNames = new Set<string>();
78✔
1709

1710
        function expressionWalker(expression) {
1711
            if (isExpression(expression)) {
120✔
1712
                expressions.push(expression);
116✔
1713
            }
1714
            if (isVariableExpression(expression)) {
120✔
1715
                variableExpressions.push(expression);
30✔
1716
                uniqueVarNames.add(expression.tokens.name.text);
30✔
1717
            }
1718
        }
1719

1720
        // Collect all expressions. Most of these expressions are fairly small so this should be quick!
1721
        // This should only be called during transpile time and only when we actually need it.
1722
        expression?.walk(expressionWalker, {
78✔
1723
            walkMode: WalkMode.visitExpressions
1724
        });
1725

1726
        //handle the expression itself (for situations when expression is a VariableExpression)
1727
        expressionWalker(expression);
78✔
1728

1729
        const scope = file.program.getFirstScopeForFile(file);
78✔
1730
        let filteredVarNames = [...uniqueVarNames];
78✔
1731
        if (scope) {
78!
1732
            filteredVarNames = filteredVarNames.filter((varName: string) => {
78✔
1733
                const varNameLower = varName.toLowerCase();
28✔
1734
                // TODO: include namespaces in this filter
1735
                return !scope.getEnumMap().has(varNameLower) &&
28✔
1736
                    !scope.getConstMap().has(varNameLower);
1737
            });
1738
        }
1739

1740
        return { expressions: expressions, varExpressions: variableExpressions, uniqueVarNames: filteredVarNames };
78✔
1741
    }
1742

1743

1744
    public concatAnnotationLeadingTrivia(stmt: Statement): Token[] {
1745
        return [...(stmt.annotations?.map(anno => anno.leadingTrivia ?? []).flat() ?? []), ...stmt.leadingTrivia];
168!
1746
    }
1747

1748
    /**
1749
     * Create a SourceNode that maps every line to itself. Useful for creating maps for files
1750
     * that haven't changed at all, but we still need the map
1751
     */
1752
    public simpleMap(source: string, src: string) {
1753
        //create a source map from the original source code
1754
        let chunks = [] as (SourceNode | string)[];
5✔
1755
        let lines = src.split(/\r?\n/g);
5✔
1756
        for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
5✔
1757
            let line = lines[lineIndex];
19✔
1758
            chunks.push(
19✔
1759
                lineIndex > 0 ? '\n' : '',
19✔
1760
                new SourceNode(lineIndex + 1, 0, source, line)
1761
            );
1762
        }
1763
        return new SourceNode(null, null, source, chunks);
5✔
1764
    }
1765

1766
    /**
1767
     * Converts a path into a standardized format (drive letter to lower, remove extra slashes, use single slash type, resolve relative parts, etc...)
1768
     */
1769
    public standardizePath(thePath: string) {
1770
        return util.driveLetterToLower(
10,819✔
1771
            rokuDeployStandardizePath(thePath)
1772
        );
1773
    }
1774

1775
    /**
1776
     * Given a Diagnostic or BsDiagnostic, return a deep clone of the diagnostic.
1777
     * @param diagnostic the diagnostic to clone
1778
     * @param relatedInformationFallbackLocation a default location to use for all `relatedInformation` entries that are missing a location
1779
     */
1780
    public toDiagnostic(diagnostic: Diagnostic | BsDiagnostic, relatedInformationFallbackLocation: string): Diagnostic {
1781
        let relatedInformation = diagnostic.relatedInformation ?? [];
15✔
1782
        if (relatedInformation.length > MAX_RELATED_INFOS_COUNT) {
15!
NEW
1783
            const relatedInfoLength = relatedInformation.length;
×
NEW
1784
            relatedInformation = relatedInformation.slice(0, MAX_RELATED_INFOS_COUNT);
×
NEW
1785
            relatedInformation.push({
×
1786
                message: `...and ${relatedInfoLength - MAX_RELATED_INFOS_COUNT} more`,
1787
                location: util.createLocationFromRange('   ', util.createRange(0, 0, 0, 0))
1788
            });
1789
        }
1790

1791
        const range = (diagnostic as BsDiagnostic).location?.range ??
15✔
1792
            (diagnostic as Diagnostic).range;
1793

1794
        let result = {
15✔
1795
            severity: diagnostic.severity,
1796
            range: range,
1797
            message: diagnostic.message,
1798
            relatedInformation: relatedInformation.map(x => {
1799

1800
                //clone related information just in case a plugin added circular ref info here
1801
                const clone = { ...x };
4✔
1802
                if (!clone.location) {
4✔
1803
                    // use the fallback location if available
1804
                    if (relatedInformationFallbackLocation) {
2✔
1805
                        clone.location = util.createLocationFromRange(relatedInformationFallbackLocation, range);
1✔
1806
                    } else {
1807
                        //remove this related information so it doesn't bring crash the language server
1808
                        return undefined;
1✔
1809
                    }
1810
                }
1811
                return clone;
3✔
1812
                //filter out null relatedInformation items
1813
            }).filter((x): x is DiagnosticRelatedInformation => Boolean(x)),
4✔
1814
            code: diagnostic.code,
1815
            source: 'brs'
1816
        } as Diagnostic;
1817
        if (diagnostic?.tags?.length > 0) {
15!
NEW
1818
            result.tags = diagnostic.tags;
×
1819
        }
1820
        return result;
15✔
1821
    }
1822

1823
    /**
1824
     * Get the first locatable item found at the specified position
1825
     * @param locatables an array of items that have a `range` property
1826
     * @param position the position that the locatable must contain
1827
     */
1828
    public getFirstLocatableAt(locatables: Locatable[], position: Position) {
1829
        for (let token of locatables) {
×
NEW
1830
            if (util.rangeContains(token.location?.range, position)) {
×
1831
                return token;
×
1832
            }
1833
        }
1834
    }
1835

1836
    /**
1837
     * Sort an array of objects that have a Range
1838
     */
1839
    public sortByRange<T extends { range: Range | undefined }>(locatables: T[]) {
1840
        //sort the tokens by range
1841
        return locatables.sort((a, b) => {
27✔
1842
            //handle undefined tokens to prevent crashes
1843
            if (!a?.range) {
252!
1844
                return 1;
1✔
1845
            }
1846
            if (!b?.range) {
251!
NEW
1847
                return -1;
×
1848
            }
1849

1850
            //start line
1851
            if (a.range.start.line < b.range.start.line) {
251!
UNCOV
1852
                return -1;
×
1853
            }
1854
            if (a.range.start.line > b.range.start.line) {
251✔
1855
                return 1;
152✔
1856
            }
1857
            //start char
1858
            if (a.range.start.character < b.range.start.character) {
99✔
1859
                return -1;
60✔
1860
            }
1861
            if (a.range.start.character > b.range.start.character) {
39!
1862
                return 1;
39✔
1863
            }
1864
            //end line
1865
            if (a.range.end.line < b.range.end.line) {
×
1866
                return -1;
×
1867
            }
1868
            if (a.range.end.line > b.range.end.line) {
×
1869
                return 1;
×
1870
            }
1871
            //end char
1872
            if (a.range.end.character < b.range.end.character) {
×
1873
                return -1;
×
1874
            } else if (a.range.end.character > b.range.end.character) {
×
1875
                return 1;
×
1876
            }
1877
            return 0;
×
1878
        });
1879
    }
1880

1881
    /**
1882
     * Split the given text and return ranges for each chunk.
1883
     * Only works for single-line strings
1884
     */
1885
    public splitGetRange(separator: string, text: string, range: Range) {
1886
        const chunks = text.split(separator);
3✔
1887
        const result = [] as Array<{ text: string; range: Range }>;
3✔
1888
        let offset = 0;
3✔
1889
        for (let chunk of chunks) {
3✔
1890
            //only keep nonzero chunks
1891
            if (chunk.length > 0) {
8✔
1892
                result.push({
7✔
1893
                    text: chunk,
1894
                    range: this.createRange(
1895
                        range.start.line,
1896
                        range.start.character + offset,
1897
                        range.end.line,
1898
                        range.start.character + offset + chunk.length
1899
                    )
1900
                });
1901
            }
1902
            offset += chunk.length + separator.length;
8✔
1903
        }
1904
        return result;
3✔
1905
    }
1906

1907
    /**
1908
     * Wrap the given code in a markdown code fence (with the language)
1909
     */
1910
    public mdFence(code: string, language = '') {
×
1911
        return '```' + language + '\n' + code + '\n```';
120✔
1912
    }
1913

1914
    /**
1915
     * Gets each part of the dotted get.
1916
     * @param node any ast expression
1917
     * @returns an array of the parts of the dotted get. If not fully a dotted get, then returns undefined
1918
     */
1919
    public getAllDottedGetParts(node: AstNode): Identifier[] | undefined {
1920
        //this is a hot function and has been optimized. Don't rewrite unless necessary
1921
        const parts: Identifier[] = [];
9,206✔
1922
        let nextPart = node;
9,206✔
1923
        loop: while (nextPart) {
9,206✔
1924
            switch (nextPart?.kind) {
13,819!
1925
                case AstNodeKind.AssignmentStatement:
13,819!
1926
                    return [(node as AssignmentStatement).tokens.name];
9✔
1927
                case AstNodeKind.DottedGetExpression:
1928
                    parts.push((nextPart as DottedGetExpression)?.tokens.name);
4,540!
1929
                    nextPart = (nextPart as DottedGetExpression).obj;
4,540✔
1930
                    continue;
4,540✔
1931
                case AstNodeKind.CallExpression:
1932
                    nextPart = (nextPart as CallExpression).callee;
39✔
1933
                    continue;
39✔
1934
                case AstNodeKind.TypeExpression:
NEW
1935
                    nextPart = (nextPart as TypeExpression).expression;
×
NEW
1936
                    continue;
×
1937
                case AstNodeKind.VariableExpression:
1938
                    parts.push((nextPart as VariableExpression)?.tokens.name);
9,098!
1939
                    break loop;
9,098✔
1940
                case AstNodeKind.LiteralExpression:
1941
                    parts.push((nextPart as LiteralExpression)?.tokens.value as Identifier);
4!
1942
                    break loop;
4✔
1943
                case AstNodeKind.IndexedGetExpression:
1944
                    nextPart = (nextPart as unknown as IndexedGetExpression).obj;
35✔
1945
                    continue;
35✔
1946
                case AstNodeKind.FunctionParameterExpression:
1947
                    return [(nextPart as FunctionParameterExpression).tokens.name];
6✔
1948
                case AstNodeKind.GroupingExpression:
1949
                    parts.push(createIdentifier('()', nextPart.location));
6✔
1950
                    break loop;
6✔
1951
                default:
1952
                    //we found a non-DottedGet expression, so return because this whole operation is invalid.
1953
                    return undefined;
82✔
1954
            }
1955
        }
1956
        return parts.reverse();
9,109✔
1957
    }
1958

1959
    /**
1960
     * Given an expression, return all the DottedGet name parts as a string.
1961
     * Mostly used to convert namespaced item full names to a strings
1962
     */
1963
    public getAllDottedGetPartsAsString(node: Expression | Statement, parseMode = ParseMode.BrighterScript): string {
1,854✔
1964
        //this is a hot function and has been optimized. Don't rewrite unless necessary
1965
        /* eslint-disable no-var */
1966
        var sep = parseMode === ParseMode.BrighterScript ? '.' : '_';
8,096✔
1967
        const parts = this.getAllDottedGetParts(node) ?? [];
8,096✔
1968
        var result = parts[0]?.text;
8,096✔
1969
        for (var i = 1; i < parts.length; i++) {
8,096✔
1970
            result += sep + parts[i].text;
3,802✔
1971
        }
1972
        return result;
8,096✔
1973
        /* eslint-enable no-var */
1974
    }
1975

1976
    public stringJoin(strings: string[], separator: string) {
1977
        // eslint-disable-next-line no-var
NEW
1978
        var result = strings[0] ?? '';
×
1979
        // eslint-disable-next-line no-var
NEW
1980
        for (var i = 1; i < strings.length; i++) {
×
NEW
1981
            result += separator + strings[i];
×
1982
        }
NEW
1983
        return result;
×
1984
    }
1985

1986
    /**
1987
     * Break an expression into each part.
1988
     */
1989
    public splitExpression(expression: Expression) {
1990
        const parts: Expression[] = [expression];
9,517✔
1991
        let nextPart = expression;
9,517✔
1992
        while (nextPart) {
9,517✔
1993
            if (isDottedGetExpression(nextPart) || isIndexedGetExpression(nextPart) || isXmlAttributeGetExpression(nextPart)) {
11,953✔
1994
                nextPart = nextPart.obj;
954✔
1995

1996
            } else if (isCallExpression(nextPart) || isCallfuncExpression(nextPart)) {
10,999✔
1997
                nextPart = nextPart.callee;
1,482✔
1998

1999
            } else if (isTypeExpression(nextPart)) {
9,517!
2000
                nextPart = nextPart.expression;
×
2001
            } else {
2002
                break;
9,517✔
2003
            }
2004
            parts.unshift(nextPart);
2,436✔
2005
        }
2006
        return parts;
9,517✔
2007
    }
2008

2009
    /**
2010
     * Break an expression into each part, and return any VariableExpression or DottedGet expresisons from left-to-right.
2011
     */
2012
    public getDottedGetPath(expression: Expression): [VariableExpression, ...DottedGetExpression[]] {
UNCOV
2013
        let parts: Expression[] = [];
×
UNCOV
2014
        let nextPart = expression;
×
NEW
2015
        loop: while (nextPart) {
×
NEW
2016
            switch (nextPart?.kind) {
×
2017
                case AstNodeKind.DottedGetExpression:
×
NEW
2018
                    parts.push(nextPart);
×
NEW
2019
                    nextPart = (nextPart as DottedGetExpression).obj;
×
NEW
2020
                    continue;
×
2021
                case AstNodeKind.IndexedGetExpression:
2022
                case AstNodeKind.XmlAttributeGetExpression:
NEW
2023
                    nextPart = (nextPart as IndexedGetExpression | XmlAttributeGetExpression).obj;
×
NEW
2024
                    parts = [];
×
NEW
2025
                    continue;
×
2026
                case AstNodeKind.CallExpression:
2027
                case AstNodeKind.CallfuncExpression:
NEW
2028
                    nextPart = (nextPart as CallExpression | CallfuncExpression).callee;
×
NEW
2029
                    parts = [];
×
NEW
2030
                    continue;
×
2031
                case AstNodeKind.NewExpression:
NEW
2032
                    nextPart = (nextPart as NewExpression).call.callee;
×
NEW
2033
                    parts = [];
×
NEW
2034
                    continue;
×
2035
                case AstNodeKind.TypeExpression:
NEW
2036
                    nextPart = (nextPart as TypeExpression).expression;
×
NEW
2037
                    continue;
×
2038
                case AstNodeKind.VariableExpression:
NEW
2039
                    parts.push(nextPart);
×
NEW
2040
                    break loop;
×
2041
                default:
NEW
2042
                    return [] as any;
×
2043
            }
2044
        }
NEW
2045
        return parts.reverse() as any;
×
2046
    }
2047

2048
    /**
2049
     * Returns an integer if valid, or undefined. Eliminates checking for NaN
2050
     */
2051
    public parseInt(value: any) {
2052
        const result = parseInt(value);
34✔
2053
        if (!isNaN(result)) {
34✔
2054
            return result;
29✔
2055
        } else {
2056
            return undefined;
5✔
2057
        }
2058
    }
2059

2060
    /**
2061
     * Converts a range to a string in the format 1:2-3:4
2062
     */
2063
    public rangeToString(range: Range) {
2064
        return `${range?.start?.line}:${range?.start?.character}-${range?.end?.line}:${range?.end?.character}`;
1,812✔
2065
    }
2066

2067
    public validateTooDeepFile(file: (BrsFile | XmlFile)) {
2068
        //find any files nested too deep
2069
        let destPath = file?.destPath?.toString();
1,860!
2070
        let rootFolder = destPath?.replace(/^pkg:/, '').split(/[\\\/]/)[0].toLowerCase();
1,860!
2071

2072
        if (isBrsFile(file) && rootFolder !== 'source') {
1,860✔
2073
            return;
277✔
2074
        }
2075

2076
        if (isXmlFile(file) && rootFolder !== 'components') {
1,583!
2077
            return;
×
2078
        }
2079

2080
        let fileDepth = this.getParentDirectoryCount(destPath);
1,583✔
2081
        if (fileDepth >= 8) {
1,583✔
2082
            file.program?.diagnostics.register({
3!
2083
                ...DiagnosticMessages.detectedTooDeepFileSource(fileDepth),
2084
                location: util.createLocationFromFileRange(file, this.createRange(0, 0, 0, Number.MAX_VALUE))
2085
            });
2086
        }
2087
    }
2088

2089
    /**
2090
     * Wraps SourceNode's constructor to be compatible with the TranspileResult type
2091
     */
2092
    public sourceNodeFromTranspileResult(
2093
        line: number | null,
2094
        column: number | null,
2095
        source: string | null,
2096
        chunks?: string | SourceNode | TranspileResult,
2097
        name?: string
2098
    ): SourceNode {
2099
        // we can use a typecast rather than actually transforming the data because SourceNode
2100
        // accepts a more permissive type than its typedef states
2101
        return new SourceNode(line, column, source, chunks as any, name);
7,392✔
2102
    }
2103

2104
    /**
2105
     * Find the index of the last item in the array that matches.
2106
     */
2107
    public findLastIndex<T>(array: T[], matcher: (T) => boolean) {
2108
        for (let i = array.length - 1; i >= 0; i--) {
27✔
2109
            if (matcher(array[i])) {
24✔
2110
                return i;
16✔
2111
            }
2112
        }
2113
    }
2114

2115
    public processTypeChain(typeChain: TypeChainEntry[]): TypeChainProcessResult {
2116
        let fullChainName = '';
1,055✔
2117
        let fullErrorName = '';
1,055✔
2118
        let itemName = '';
1,055✔
2119
        let previousTypeName = '';
1,055✔
2120
        let parentTypeName = '';
1,055✔
2121
        let itemTypeKind = '';
1,055✔
2122
        let parentTypeKind = '';
1,055✔
2123
        let astNode: AstNode;
2124
        let errorLocation: Location;
2125
        let containsDynamic = false;
1,055✔
2126
        let continueResolvingAllItems = true;
1,055✔
2127
        for (let i = 0; i < typeChain.length; i++) {
1,055✔
2128
            const chainItem = typeChain[i];
2,100✔
2129
            const dotSep = chainItem.separatorToken?.text ?? '.';
2,100!
2130
            if (i > 0) {
2,100✔
2131
                fullChainName += dotSep;
1,047✔
2132
            }
2133
            fullChainName += chainItem.name;
2,100✔
2134
            if (continueResolvingAllItems) {
2,100✔
2135
                parentTypeName = previousTypeName;
1,910✔
2136
                parentTypeKind = itemTypeKind;
1,910✔
2137
                fullErrorName = previousTypeName ? `${previousTypeName}${dotSep}${chainItem.name}` : chainItem.name;
1,910✔
2138
                itemTypeKind = (chainItem.type as any)?.kind;
1,910✔
2139

2140
                let typeString = chainItem.type?.toString();
1,910✔
2141
                let typeToFindStringFor = chainItem.type;
1,910✔
2142
                while (typeToFindStringFor) {
1,910✔
2143
                    if (isUnionType(chainItem.type)) {
1,906✔
2144
                        typeString = `(${typeToFindStringFor.toString()})`;
7✔
2145
                        break;
7✔
2146
                    } else if (isCallableType(typeToFindStringFor)) {
1,899✔
2147
                        if (isTypedFunctionType(typeToFindStringFor) && i < typeChain.length - 1) {
37✔
2148
                            typeToFindStringFor = typeToFindStringFor.returnType;
10✔
2149
                        } else {
2150
                            typeString = 'function';
27✔
2151
                            break;
27✔
2152
                        }
2153
                        parentTypeName = previousTypeName;
10✔
2154
                    } else if (isNamespaceType(typeToFindStringFor) && parentTypeName) {
1,862✔
2155
                        const chainItemTypeName = typeToFindStringFor.toString();
334✔
2156
                        typeString = parentTypeName + '.' + chainItemTypeName;
334✔
2157
                        if (chainItemTypeName.toLowerCase().startsWith(parentTypeName.toLowerCase())) {
334✔
2158
                            // the following namespace already knows...
2159
                            typeString = chainItemTypeName;
330✔
2160
                        }
2161
                        break;
334✔
2162
                    } else {
2163
                        typeString = typeToFindStringFor?.toString();
1,528!
2164
                        break;
1,528✔
2165
                    }
2166
                }
2167

2168
                previousTypeName = typeString ?? '';
1,910✔
2169
                itemName = chainItem.name;
1,910✔
2170
                astNode = chainItem.astNode;
1,910✔
2171
                containsDynamic = containsDynamic || (isDynamicType(chainItem.type) && !isAnyReferenceType(chainItem.type));
1,910✔
2172
                if (!chainItem.isResolved) {
1,910✔
2173
                    errorLocation = chainItem.location;
920✔
2174
                    continueResolvingAllItems = false;
920✔
2175
                }
2176
            }
2177
        }
2178
        return {
1,055✔
2179
            itemName: itemName,
2180
            itemTypeKind: itemTypeKind,
2181
            itemParentTypeName: parentTypeName,
2182
            itemParentTypeKind: parentTypeKind,
2183
            fullNameOfItem: fullErrorName,
2184
            fullChainName: fullChainName,
2185
            location: errorLocation,
2186
            containsDynamic: containsDynamic,
2187
            astNode: astNode
2188
        };
2189
    }
2190

2191

2192
    public isInTypeExpression(expression: AstNode): boolean {
2193
        //TODO: this is much faster than node.findAncestor(), but may need to be updated for "complicated" type expressions
2194
        if (isTypeExpression(expression) ||
14,696✔
2195
            isTypeExpression(expression.parent) ||
2196
            isTypedArrayExpression(expression) ||
2197
            isTypedArrayExpression(expression.parent)) {
2198
            return true;
4,326✔
2199
        }
2200
        if (isBinaryExpression(expression.parent)) {
10,370✔
2201
            let currentExpr: AstNode = expression.parent;
2,035✔
2202
            while (isBinaryExpression(currentExpr) && currentExpr.tokens.operator.kind === TokenKind.Or) {
2,035✔
2203
                currentExpr = currentExpr.parent;
118✔
2204
            }
2205
            return isTypeExpression(currentExpr) || isTypedArrayExpression(currentExpr);
2,035✔
2206
        }
2207
        return false;
8,335✔
2208
    }
2209

2210
    public hasAnyRequiredSymbolChanged(requiredSymbols: UnresolvedSymbol[], changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
2211
        if (!requiredSymbols || !changedSymbols) {
1,753!
NEW
2212
            return false;
×
2213
        }
2214
        const runTimeChanges = changedSymbols.get(SymbolTypeFlag.runtime);
1,753✔
2215
        const typeTimeChanges = changedSymbols.get(SymbolTypeFlag.typetime);
1,753✔
2216

2217
        for (const symbol of requiredSymbols) {
1,753✔
2218
            if (this.setContainsUnresolvedSymbol(runTimeChanges, symbol) || this.setContainsUnresolvedSymbol(typeTimeChanges, symbol)) {
635✔
2219
                return true;
297✔
2220
            }
2221
        }
2222

2223
        return false;
1,456✔
2224
    }
2225

2226
    public setContainsUnresolvedSymbol(symbolLowerNameSet: Set<string>, symbol: UnresolvedSymbol) {
2227
        if (!symbolLowerNameSet || symbolLowerNameSet.size === 0) {
984✔
2228
            return false;
361✔
2229
        }
2230

2231
        for (const possibleNameLower of symbol.lookups) {
623✔
2232
            if (symbolLowerNameSet.has(possibleNameLower)) {
2,238✔
2233
                return true;
297✔
2234
            }
2235
        }
2236
        return false;
326✔
2237
    }
2238

2239
    public truncate<T>(options: {
2240
        leadingText: string;
2241
        items: T[];
2242
        trailingText?: string;
2243
        maxLength: number;
2244
        itemSeparator?: string;
2245
        partBuilder?: (item: T) => string;
2246
    }): string {
2247
        let leadingText = options.leadingText;
19✔
2248
        let items = options?.items ?? [];
19!
2249
        let trailingText = options?.trailingText ?? '';
19!
2250
        let maxLength = options?.maxLength ?? 160;
19!
2251
        let itemSeparator = options?.itemSeparator ?? ', ';
19!
2252
        let partBuilder = options?.partBuilder ?? ((x) => x.toString());
19!
2253

2254
        let parts = [];
19✔
2255
        let length = leadingText.length + (trailingText?.length ?? 0);
19!
2256

2257
        //calculate the max number of items we could fit in the given space
2258
        for (let i = 0; i < items.length; i++) {
19✔
2259
            let part = partBuilder(items[i]);
91✔
2260
            if (i > 0) {
91✔
2261
                part = itemSeparator + part;
72✔
2262
            }
2263
            parts.push(part);
91✔
2264
            length += part.length;
91✔
2265
            //exit the loop if we've maxed out our length
2266
            if (length >= maxLength) {
91✔
2267
                break;
6✔
2268
            }
2269
        }
2270
        let message: string;
2271
        //we have enough space to include all the parts
2272
        if (parts.length >= items.length) {
19✔
2273
            message = leadingText + parts.join('') + trailingText;
13✔
2274

2275
            //we require truncation
2276
        } else {
2277
            //account for truncation message length including max possible "more" items digits, trailing text length, and the separator between last item and trailing text
2278
            length = leadingText.length + `...and ${items.length} more`.length + itemSeparator.length + (trailingText?.length ?? 0);
6!
2279
            message = leadingText;
6✔
2280
            for (let i = 0; i < parts.length; i++) {
6✔
2281
                //always include at least 2 items. if this part would overflow the max, then skip it and finalize the message
2282
                if (i > 1 && length + parts[i].length > maxLength) {
47✔
2283
                    message += itemSeparator + `...and ${items.length - i} more` + trailingText;
6✔
2284
                    return message;
6✔
2285
                } else {
2286
                    message += parts[i];
41✔
2287
                    length += parts[i].length;
41✔
2288
                }
2289
            }
2290
        }
2291
        return message;
13✔
2292
    }
2293

2294
    public getAstNodeFriendlyName(node: AstNode) {
2295
        return node?.kind.replace(/Statement|Expression/g, '');
224!
2296
    }
2297

2298

2299
    public hasLeadingComments(input: Token | AstNode) {
2300
        const leadingTrivia = isToken(input) ? input?.leadingTrivia : input?.leadingTrivia ?? [];
6,526!
2301
        return !!leadingTrivia.find(t => t.kind === TokenKind.Comment);
13,331✔
2302
    }
2303

2304
    public getLeadingComments(input: Token | AstNode) {
2305
        const leadingTrivia = isToken(input) ? input?.leadingTrivia : input?.leadingTrivia ?? [];
10,379!
2306
        return leadingTrivia.filter(t => t.kind === TokenKind.Comment);
31,975✔
2307
    }
2308

2309
    public isLeadingCommentOnSameLine(line: RangeLike, input: Token | AstNode) {
2310
        const leadingCommentRange = this.getLeadingComments(input)?.[0];
9,612!
2311
        if (leadingCommentRange) {
9,612✔
2312
            return this.linesTouch(line, leadingCommentRange?.location);
1,420!
2313
        }
2314
        return false;
8,192✔
2315
    }
2316

2317
    public isClassUsedAsFunction(potentialClassType: BscType, expression: Expression, options: GetTypeOptions) {
2318
        // eslint-disable-next-line no-bitwise
2319
        if ((options?.flags ?? 0) & SymbolTypeFlag.runtime &&
15,370!
2320
            isClassType(potentialClassType) &&
2321
            !options.isExistenceTest &&
2322
            potentialClassType.name.toLowerCase() === this.getAllDottedGetPartsAsString(expression).toLowerCase() &&
2323
            !expression.findAncestor(isNewExpression)) {
2324
            return true;
30✔
2325
        }
2326
        return false;
15,340✔
2327
    }
2328

2329
    public getSpecialCaseCallExpressionReturnType(callExpr: CallExpression) {
2330
        if (isVariableExpression(callExpr.callee) && callExpr.callee.tokens.name.text.toLowerCase() === 'createobject') {
615✔
2331
            const componentName = isLiteralString(callExpr.args[0]) ? callExpr.args[0].tokens.value?.text?.replace(/"/g, '') : '';
111!
2332
            const nodeType = componentName.toLowerCase() === 'rosgnode' && isLiteralString(callExpr.args[1]) ? callExpr.args[1].tokens.value?.text?.replace(/"/g, '') : '';
111!
2333
            if (componentName?.toLowerCase().startsWith('ro')) {
111!
2334
                const fullName = componentName + nodeType;
97✔
2335
                const data = {};
97✔
2336
                const symbolTable = callExpr.getSymbolTable();
97✔
2337
                const foundType = symbolTable.getSymbolType(fullName, {
97✔
2338
                    flags: SymbolTypeFlag.typetime,
2339
                    data: data,
2340
                    tableProvider: () => callExpr?.getSymbolTable(),
117!
2341
                    fullName: fullName
2342
                });
2343
                if (foundType) {
97!
2344
                    return foundType;
97✔
2345
                }
2346
            }
2347
        }
2348
    }
2349
}
2350

2351
/**
2352
 * 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,
2353
 * we can't use `object.tag` syntax.
2354
 */
2355
export function standardizePath(stringParts, ...expressions: any[]) {
1✔
2356
    let result: string[] = [];
20,990✔
2357
    for (let i = 0; i < stringParts.length; i++) {
20,990✔
2358
        result.push(stringParts[i], expressions[i]);
178,135✔
2359
    }
2360
    return util.driveLetterToLower(
20,990✔
2361
        rokuDeployStandardizePath(
2362
            result.join('')
2363
        )
2364
    );
2365
}
2366

2367
/**
2368
 * An item that can be coerced into a `Range`
2369
 */
2370
export type RangeLike = { location?: Location } | Location | { range?: Range } | Range | undefined;
2371

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