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

rokucommunity / brighterscript / #13125

01 Oct 2024 02:12PM UTC coverage: 86.842% (-1.4%) from 88.193%
#13125

push

web-flow
Merge d4a9e5fff into 3a2dc7282

11554 of 14068 branches covered (82.13%)

Branch coverage included in aggregate %.

7000 of 7592 new or added lines in 100 files covered. (92.2%)

83 existing lines in 18 files now uncovered.

12701 of 13862 relevant lines covered (91.62%)

29529.09 hits per line

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

86.18
/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, ExtraSymbolData } 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, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberType, isReferenceType, 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, NamespaceStatement } from './parser/Statement';
50
import type { BscFile } from './files/BscFile';
51
import type { NamespaceType } from './types/NamespaceType';
52

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

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

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

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

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

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

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

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

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

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

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

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

217
            let projectFileCwd = path.dirname(configFilePath);
27✔
218

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

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

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

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

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

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

290
        let result: T;
291
        let err;
292

UNCOV
293
        try {
×
UNCOV
294
            result = callback();
×
295
        } catch (e) {
296
            err = e;
×
297
        }
298

UNCOV
299
        if (targetCwd) {
×
UNCOV
300
            process.chdir(originalCwd);
×
301
        }
302

UNCOV
303
        if (err) {
×
304
            throw err;
×
305
        } else {
306
            //justification: `result` is set as long as `err` is not set and vice versa
UNCOV
307
            return result!;
×
308
        }
309
    }
310

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

319
        if (config?.noProject) {
74!
320
            return result;
1✔
321
        }
322

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

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

346
        const cwd = config.cwd ?? process.cwd();
1,900✔
347
        const rootFolderName = path.basename(cwd);
1,900✔
348
        const retainStagingDir = (config.retainStagingDir ?? config.retainStagingDir) === true ? true : false;
1,900✔
349

350
        let logLevel: LogLevel = LogLevel.log;
1,900✔
351

352
        if (typeof config.logLevel === 'string') {
1,900!
353
            logLevel = LogLevel[(config.logLevel as string).toLowerCase()] ?? LogLevel.log;
×
354
        }
355

356
        let bslibDestinationDir = config.bslibDestinationDir ?? 'source';
1,900✔
357
        if (bslibDestinationDir !== 'source') {
1,900✔
358
            // strip leading and trailing slashes
359
            bslibDestinationDir = bslibDestinationDir.replace(/^(\/*)(.*?)(\/*)$/, '$2');
4✔
360
        }
361

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

392
        //mutate `config` in case anyone is holding a reference to the incomplete one
393
        const merged: FinalizedBsConfig = Object.assign(config, configWithDefaults);
1,900✔
394

395
        return merged;
1,900✔
396
    }
397

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

412
        rootDir = path.resolve(cwd, rootDir);
1,789✔
413

414
        return rootDir;
1,789✔
415
    }
416

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

424
        for (let callableContainer of callables) {
1,603✔
425
            let lowerName = callableContainer.callable.getName(ParseMode.BrightScript).toLowerCase();
126,800✔
426

427
            //create a new array for this name
428
            const list = result.get(lowerName);
126,800✔
429
            if (list) {
126,800✔
430
                list.push(callableContainer);
6,446✔
431
            } else {
432
                result.set(lowerName, [callableContainer]);
120,354✔
433
            }
434
        }
435
        return result;
1,603✔
436
    }
437

438
    /**
439
     * Split a file by newline characters (LF or CRLF)
440
     */
441
    public getLines(text: string) {
442
        return text.split(/\r?\n/);
×
443
    }
444

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

467
        //remove the filename
468
        let containingFolder = path.normalize(path.dirname(containingFilePathAbsolute));
208✔
469
        //start with the containing folder, split by slash
470
        let result = containingFolder.split(path.sep);
208✔
471

472
        //split on slash
473
        let targetParts = path.normalize(targetPath).split(path.sep);
208✔
474

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

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

499
        //break by path separator
500
        let sourceParts = pkgSrcPath.split(path.sep);
11✔
501
        let targetParts = pkgTargetPath.split(path.sep);
11✔
502

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

514
        //throw out the common parts from both sets
515
        sourceParts.splice(0, commonParts.length);
11✔
516
        targetParts.splice(0, commonParts.length);
11✔
517

518
        //throw out the filename part of source
519
        sourceParts.splice(sourceParts.length - 1, 1);
11✔
520
        //start out by adding updir paths for each remaining source part
521
        let resultParts = sourceParts.map(() => '..');
11✔
522

523
        //now add every target part
524
        resultParts = [...resultParts, ...targetParts];
11✔
525
        return path.join(...resultParts);
11✔
526
    }
527

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

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

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

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

575
        // Check if `b` is before `a`
576
        if (b.end.line < a.start.line || (b.end.line === a.start.line && b.end.character <= a.start.character)) {
8✔
577
            return false;
1✔
578
        }
579

580
        // These ranges must intersect
581
        return true;
7✔
582
    }
583

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

603
        // Check if `b` is before `a`
604
        if (b.end.line < a.start.line || (b.end.line === a.start.line && b.end.character < a.start.character)) {
23✔
605
            return false;
2✔
606
        }
607

608
        // These ranges must intersect
609
        return true;
21✔
610
    }
611

612
    /**
613
     * Test if `position` is in `range`. If the position is at the edges, will return true.
614
     * Adapted from core vscode
615
     */
616
    public rangeContains(range: Range | undefined, position: Position | undefined) {
617
        return this.comparePositionToRange(position, range) === 0;
11,624✔
618
    }
619

620
    public comparePositionToRange(position: Position | undefined, range: Range | undefined) {
621
        //stop if the either range is missng
622
        if (!position || !range) {
13,603✔
623
            return 0;
10✔
624
        }
625

626
        if (this.comparePosition(position, range.start) < 0) {
13,593✔
627
            return -1;
2,116✔
628
        }
629
        if (this.comparePosition(position, range.end) > 0) {
11,477✔
630
            return 1;
8,208✔
631
        }
632
        return 0;
3,269✔
633
    }
634

635
    public comparePosition(a: Position | undefined, b: Position) {
636
        //stop if the either position is missing
637
        if (!a || !b) {
208,949!
NEW
638
            return 0;
×
639
        }
640

641
        if (a.line < b.line || (a.line === b.line && a.character < b.character)) {
208,949✔
642
            return -1;
14,898✔
643
        }
644
        if (a.line > b.line || (a.line === b.line && a.character > b.character)) {
194,051✔
645
            return 1;
193,229✔
646
        }
647
        return 0;
822✔
648
    }
649

650
    /**
651
     * Combine all the documentation for a node - uses the AstNode's leadingTrivia property
652
     * @param node the node to get the documentation for
653
     * @param options extra options
654
     * @param options.prettyPrint if true, will format the comment text for markdown
655
     * @param options.commentTokens out Array of tokens that match the comment lines
656
     */
657
    public getNodeDocumentation(node: AstNode, options: { prettyPrint?: boolean; commentTokens?: Token[] } = { prettyPrint: true }) {
935✔
658
        if (!node) {
29,925✔
659
            return '';
1,231✔
660
        }
661
        options = options ?? { prettyPrint: true };
28,694!
662
        options.commentTokens = options.commentTokens ?? [];
28,694✔
663
        const nodeTrivia = node.leadingTrivia ?? [];
28,694!
664
        const leadingTrivia = isStatement(node)
28,694✔
665
            ? [...(node.annotations?.map(anno => anno.leadingTrivia ?? []).flat() ?? []), ...nodeTrivia]
32!
666
            : nodeTrivia;
667
        const tokens = leadingTrivia?.filter(t => t.kind === TokenKind.Newline || t.kind === TokenKind.Comment);
55,271!
668
        const comments = [] as Token[];
28,694✔
669

670
        let newLinesInRow = 0;
28,694✔
671
        for (let i = tokens.length - 1; i >= 0; i--) {
28,694✔
672
            const token = tokens[i];
25,282✔
673
            //skip whitespace and newline chars
674
            if (token.kind === TokenKind.Comment) {
25,282✔
675
                comments.push(token);
942✔
676
                newLinesInRow = 0;
942✔
677
            } else if (token.kind === TokenKind.Newline) {
24,340!
678
                //skip these tokens
679
                newLinesInRow++;
24,340✔
680

681
                if (newLinesInRow > 1) {
24,340✔
682
                    // stop processing on empty line.
683
                    break;
2,839✔
684
                }
685
                //any other token means there are no more comments
686
            } else {
NEW
687
                break;
×
688
            }
689
        }
690
        const jsDocCommentBlockLine = /(\/\*{2,}|\*{1,}\/)/i;
28,694✔
691
        let usesjsDocCommentBlock = false;
28,694✔
692
        if (comments.length === 0) {
28,694✔
693
            return '';
27,883✔
694
        }
695
        return comments.reverse()
811✔
696
            .map(x => ({ line: x.text.replace(/^('|rem)/i, '').trim(), token: x }))
942✔
697
            .filter(({ line }) => {
698
                if (jsDocCommentBlockLine.exec(line)) {
942✔
699
                    usesjsDocCommentBlock = true;
30✔
700
                    return false;
30✔
701
                }
702
                return true;
912✔
703
            }).map(({ line, token }) => {
704
                if (usesjsDocCommentBlock) {
912✔
705
                    if (line.startsWith('*')) {
23✔
706
                        //remove jsDoc leading '*'
707
                        line = line.slice(1).trim();
22✔
708
                    }
709
                }
710
                if (options.prettyPrint && line.startsWith('@')) {
912✔
711
                    // Handle jsdoc/brightscriptdoc tags specially
712
                    // make sure they are on their own markdown line, and add italics
713
                    const firstSpaceIndex = line.indexOf(' ');
3✔
714
                    if (firstSpaceIndex === -1) {
3✔
715
                        return `\n_${line}_`;
1✔
716
                    }
717
                    const firstWord = line.substring(0, firstSpaceIndex);
2✔
718
                    return `\n_${firstWord}_ ${line.substring(firstSpaceIndex + 1)}`;
2✔
719
                }
720
                if (options.commentTokens) {
909!
721
                    options.commentTokens.push(token);
909✔
722
                }
723
                return line;
909✔
724
            }).join('\n');
725
    }
726

727
    /**
728
     * Prefixes a component name so it can be used as type in the symbol table, without polluting available symbols
729
     *
730
     * @param sgNodeName the Name of the component
731
     * @returns the node name, prefixed with `roSGNode`
732
     */
733
    public getSgNodeTypeName(sgNodeName: string) {
734
        return 'roSGNode' + sgNodeName;
321,369✔
735
    }
736

737
    /**
738
     * Parse an xml file and get back a javascript object containing its results
739
     */
740
    public parseXml(text: string) {
741
        return new Promise<any>((resolve, reject) => {
×
742
            xml2js.parseString(text, (err, data) => {
×
743
                if (err) {
×
744
                    reject(err);
×
745
                } else {
746
                    resolve(data);
×
747
                }
748
            });
749
        });
750
    }
751

752
    public propertyCount(object: Record<string, unknown>) {
753
        let count = 0;
×
754
        for (let key in object) {
×
755
            if (object.hasOwnProperty(key)) {
×
756
                count++;
×
757
            }
758
        }
759
        return count;
×
760
    }
761

762
    public padLeft(subject: string, totalLength: number, char: string) {
763
        totalLength = totalLength > 1000 ? 1000 : totalLength;
1!
764
        while (subject.length < totalLength) {
1✔
765
            subject = char + subject;
1,000✔
766
        }
767
        return subject;
1✔
768
    }
769

770
    /**
771
     * Does the string appear to be a uri (i.e. does it start with `file:`)
772
     */
773
    public isUriLike(filePath: string) {
774
        return filePath?.indexOf('file:') === 0;// eslint-disable-line @typescript-eslint/prefer-string-starts-ends-with
275,501!
775
    }
776

777
    /**
778
     * Given a file path, convert it to a URI string
779
     */
780
    public pathToUri(filePath: string) {
781
        if (!filePath) {
245,978✔
782
            return filePath;
31,132✔
783
        } else if (this.isUriLike(filePath)) {
214,846✔
784
            return filePath;
194,684✔
785
        } else {
786
            return URI.file(filePath).toString();
20,162✔
787
        }
788
    }
789

790
    /**
791
     * Given a URI, convert that to a regular fs path
792
     */
793
    public uriToPath(uri: string) {
794
        //if this doesn't look like a URI, then assume it's already a path
795
        if (this.isUriLike(uri) === false) {
52,530✔
796
            return uri;
2✔
797
        }
798
        let parsedPath = URI.parse(uri).fsPath;
52,528✔
799

800
        //Uri annoyingly converts all drive letters to lower case...so this will bring back whatever case it came in as
801
        let match = /\/\/\/([a-z]:)/i.exec(uri);
52,528✔
802
        if (match) {
52,528✔
803
            let originalDriveCasing = match[1];
2✔
804
            parsedPath = originalDriveCasing + parsedPath.substring(2);
2✔
805
        }
806
        const normalizedPath = path.normalize(parsedPath);
52,528✔
807
        return normalizedPath;
52,528✔
808
    }
809

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

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

841
    /**
842
     * Determine if two arrays containing primitive values are equal.
843
     * This considers order and compares by equality.
844
     */
845
    public areArraysEqual(arr1: any[], arr2: any[]) {
846
        if (arr1.length !== arr2.length) {
8✔
847
            return false;
3✔
848
        }
849
        for (let i = 0; i < arr1.length; i++) {
5✔
850
            if (arr1[i] !== arr2[i]) {
7✔
851
                return false;
3✔
852
            }
853
        }
854
        return true;
2✔
855
    }
856

857
    /**
858
     * Get the outDir from options, taking into account cwd and absolute outFile paths
859
     */
860
    public getOutDir(options: FinalizedBsConfig) {
861
        options = this.normalizeConfig(options);
2✔
862
        let cwd = path.normalize(options.cwd ? options.cwd : process.cwd());
2!
863
        if (path.isAbsolute(options.outFile)) {
2!
864
            return path.dirname(options.outFile);
×
865
        } else {
866
            return path.normalize(path.join(cwd, path.dirname(options.outFile)));
2✔
867
        }
868
    }
869

870
    /**
871
     * Get paths to all files on disc that match this project's source list
872
     */
873
    public async getFilePaths(options: FinalizedBsConfig) {
874
        let rootDir = this.getRootDir(options);
48✔
875

876
        let files = await rokuDeploy.getFilePaths(options.files, rootDir);
48✔
877
        return files;
48✔
878
    }
879

880
    /**
881
     * Given a path to a brs file, compute the path to a theoretical d.bs file.
882
     * Only `.brs` files can have typedef path, so return undefined for everything else
883
     */
884
    public getTypedefPath(brsSrcPath: string) {
885
        const typedefPath = brsSrcPath
3,458✔
886
            .replace(/\.brs$/i, '.d.bs')
887
            .toLowerCase();
888

889
        if (typedefPath.endsWith('.d.bs')) {
3,458✔
890
            return typedefPath;
2,040✔
891
        } else {
892
            return undefined;
1,418✔
893
        }
894
    }
895

896

897
    /**
898
     * Walks up the chain to find the closest bsconfig.json file
899
     */
900
    public async findClosestConfigFile(currentPath: string): Promise<string | undefined> {
901
        //make the path absolute
902
        currentPath = path.resolve(
6✔
903
            path.normalize(
904
                currentPath
905
            )
906
        );
907

908
        let previousPath: string | undefined;
909
        //using ../ on the root of the drive results in the same file path, so that's how we know we reached the top
910
        while (previousPath !== currentPath) {
6✔
911
            previousPath = currentPath;
28✔
912

913
            let bsPath = path.join(currentPath, 'bsconfig.json');
28✔
914
            let brsPath = path.join(currentPath, 'brsconfig.json');
28✔
915
            if (await this.pathExists(bsPath)) {
28✔
916
                return bsPath;
2✔
917
            } else if (await this.pathExists(brsPath)) {
26✔
918
                return brsPath;
2✔
919
            } else {
920
                //walk upwards one directory
921
                currentPath = path.resolve(path.join(currentPath, '../'));
24✔
922
            }
923
        }
924
        //got to the root path, no config file exists
925
    }
926

927
    /**
928
     * Set a timeout for the specified milliseconds, and resolve the promise once the timeout is finished.
929
     * @param milliseconds the minimum number of milliseconds to sleep for
930
     */
931
    public sleep(milliseconds: number) {
932
        return new Promise((resolve) => {
103✔
933
            //if milliseconds is 0, don't actually timeout (improves unit test throughput)
934
            if (milliseconds === 0) {
103✔
935
                process.nextTick(resolve);
89✔
936
            } else {
937
                setTimeout(resolve, milliseconds);
14✔
938
            }
939
        });
940
    }
941

942
    /**
943
     * Given an array, map and then flatten
944
     * @param array the array to flatMap over
945
     * @param callback a function that is called for every array item
946
     */
947
    public flatMap<T, R>(array: T[], callback: (arg: T) => R[]): R[] {
948
        return Array.prototype.concat.apply([], array.map(callback));
16✔
949
    }
950

951
    /**
952
     * Determines if the position is greater than the range. This means
953
     * the position does not touch the range, and has a position greater than the end
954
     * of the range. A position that touches the last line/char of a range is considered greater
955
     * than the range, because the `range.end` is EXclusive
956
     */
957
    public positionIsGreaterThanRange(position: Position, range: Range) {
958

959
        //if the position is a higher line than the range
960
        if (position.line > range.end.line) {
1,166✔
961
            return true;
1,079✔
962
        } else if (position.line < range.end.line) {
87!
UNCOV
963
            return false;
×
964
        }
965
        //they are on the same line
966

967
        //if the position's char is greater than or equal to the range's
968
        if (position.character >= range.end.character) {
87!
969
            return true;
87✔
970
        } else {
UNCOV
971
            return false;
×
972
        }
973
    }
974

975
    /**
976
     * Get a range back from an object that contains (or is) a range
977
     */
978
    public extractRange(rangeIsh: RangeLike): Range | undefined {
979
        if (!rangeIsh) {
9,628✔
980
            return undefined;
29✔
981
        } else if ('location' in rangeIsh) {
9,599✔
982
            return rangeIsh.location?.range;
6,656✔
983
        } else if ('range' in rangeIsh) {
2,943!
984
            return rangeIsh.range;
2,943✔
NEW
985
        } else if (Range.is(rangeIsh)) {
×
NEW
986
            return rangeIsh;
×
987
        } else {
NEW
988
            return undefined;
×
989
        }
990
    }
991

992

993
    /**
994
     * Get a location object back by extracting location information from other objects that contain location
995
     */
996
    public getRange(startObj: | { range: Range }, endObj: { range: Range }): Range {
UNCOV
997
        if (!startObj?.range || !endObj?.range) {
×
UNCOV
998
            return undefined;
×
999
        }
UNCOV
1000
        return util.createRangeFromPositions(startObj.range?.start, endObj.range?.end);
×
1001
    }
1002

1003
    /**
1004
     * If the two items both start on the same line
1005
     */
1006
    public sameStartLine(first: { range: Range }, second: { range: Range }) {
1007
        if (first && second && first.range.start.line === second.range.start.line) {
×
1008
            return true;
×
1009
        } else {
1010
            return false;
×
1011
        }
1012
    }
1013

1014
    /**
1015
     * If the two items have lines that touch
1016
     */
1017
    public linesTouch(first: RangeLike, second: RangeLike) {
1018
        const firstRange = this.extractRange(first);
1,500✔
1019
        const secondRange = this.extractRange(second);
1,500✔
1020
        if (firstRange && secondRange && (
1,500✔
1021
            firstRange.start.line === secondRange.start.line ||
1022
            firstRange.start.line === secondRange.end.line ||
1023
            firstRange.end.line === secondRange.start.line ||
1024
            firstRange.end.line === secondRange.end.line
1025
        )) {
1026
            return true;
91✔
1027
        } else {
1028
            return false;
1,409✔
1029
        }
1030
    }
1031

1032
    /**
1033
     * Given text with (or without) dots separating text, get the rightmost word.
1034
     * (i.e. given "A.B.C", returns "C". or "B" returns "B because there's no dot)
1035
     */
1036
    public getTextAfterFinalDot(name: string) {
UNCOV
1037
        if (name) {
×
UNCOV
1038
            let parts = name.split('.');
×
UNCOV
1039
            if (parts.length > 0) {
×
UNCOV
1040
                return parts[parts.length - 1];
×
1041
            }
1042
        }
1043
    }
1044

1045
    /**
1046
     * Find a script import that the current position touches, or undefined if not found
1047
     */
1048
    public getScriptImportAtPosition(scriptImports: FileReference[], position: Position): FileReference | undefined {
1049
        let scriptImport = scriptImports.find((x) => {
117✔
1050
            return x.filePathRange &&
5✔
1051
                x.filePathRange.start.line === position.line &&
1052
                //column between start and end
1053
                position.character >= x.filePathRange.start.character &&
1054
                position.character <= x.filePathRange.end.character;
1055
        });
1056
        return scriptImport;
117✔
1057
    }
1058

1059
    /**
1060
     * Given the class name text, return a namespace-prefixed name.
1061
     * If the name already has a period in it, or the namespaceName was not provided, return the class name as is.
1062
     * If the name does not have a period, and a namespaceName was provided, return the class name prepended by the namespace name.
1063
     * If no namespace is provided, return the `className` unchanged.
1064
     */
1065
    public getFullyQualifiedClassName(className: string, namespaceName?: string) {
1066
        if (className?.includes('.') === false && namespaceName) {
3,795✔
1067
            return `${namespaceName}.${className}`;
204✔
1068
        } else {
1069
            return className;
3,591✔
1070
        }
1071
    }
1072

1073
    public splitIntoLines(string: string) {
1074
        return string.split(/\r?\n/g);
169✔
1075
    }
1076

1077
    public getTextForRange(string: string | string[], range: Range): string {
1078
        let lines: string[];
1079
        if (Array.isArray(string)) {
171✔
1080
            lines = string;
170✔
1081
        } else {
1082
            lines = this.splitIntoLines(string);
1✔
1083
        }
1084

1085
        const start = range.start;
171✔
1086
        const end = range.end;
171✔
1087

1088
        let endCharacter = end.character;
171✔
1089
        // If lines are the same we need to subtract out our new starting position to make it work correctly
1090
        if (start.line === end.line) {
171✔
1091
            endCharacter -= start.character;
159✔
1092
        }
1093

1094
        let rangeLines = [lines[start.line].substring(start.character)];
171✔
1095
        for (let i = start.line + 1; i <= end.line; i++) {
171✔
1096
            rangeLines.push(lines[i]);
12✔
1097
        }
1098
        const lastLine = rangeLines.pop();
171✔
1099
        if (lastLine !== undefined) {
171!
1100
            rangeLines.push(lastLine.substring(0, endCharacter));
171✔
1101
        }
1102
        return rangeLines.join('\n');
171✔
1103
    }
1104

1105
    /**
1106
     * Helper for creating `Location` objects. Prefer using this function because vscode-languageserver's `Location.create()` is significantly slower at scale
1107
     */
1108
    public createLocationFromRange(uri: string, range: Range): Location {
1109
        return {
7,753✔
1110
            uri: util.pathToUri(uri),
1111
            range: range
1112
        };
1113
    }
1114

1115
    /**
1116
     * Helper for creating `Location` objects from a file and range
1117
     */
1118
    public createLocationFromFileRange(file: BscFile, range: Range): Location {
1119
        return this.createLocationFromRange(this.pathToUri(file?.srcPath), range);
379!
1120
    }
1121

1122
    /**
1123
     * 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
1124
     */
1125
    public createLocation(startLine: number, startCharacter: number, endLine: number, endCharacter: number, uri?: string): Location {
1126
        return {
229,460✔
1127
            uri: util.pathToUri(uri),
1128
            range: {
1129
                start: {
1130
                    line: startLine,
1131
                    character: startCharacter
1132
                },
1133
                end: {
1134
                    line: endLine,
1135
                    character: endCharacter
1136
                }
1137
            }
1138
        };
1139
    }
1140

1141
    /**
1142
     * Helper for creating `Range` objects. Prefer using this function because vscode-languageserver's `Range.create()` is significantly slower.
1143
     */
1144
    public createRange(startLine: number, startCharacter: number, endLine: number, endCharacter: number): Range {
1145
        return {
7,798✔
1146
            start: {
1147
                line: startLine,
1148
                character: startCharacter
1149
            },
1150
            end: {
1151
                line: endLine,
1152
                character: endCharacter
1153
            }
1154
        };
1155
    }
1156

1157
    /**
1158
     * Create a `Range` from two `Position`s
1159
     */
1160
    public createRangeFromPositions(startPosition: Position, endPosition: Position): Range | undefined {
1161
        startPosition = startPosition ?? endPosition;
171✔
1162
        endPosition = endPosition ?? startPosition;
171✔
1163
        if (!startPosition && !endPosition) {
171!
NEW
1164
            return undefined;
×
1165
        }
1166
        return this.createRange(startPosition.line, startPosition.character, endPosition.line, endPosition.character);
171✔
1167
    }
1168

1169
    /**
1170
     * Clone a range
1171
     */
1172
    public cloneLocation(location: Location) {
1173
        if (location) {
14,512✔
1174
            return {
14,347✔
1175
                uri: location.uri,
1176
                range: {
1177
                    start: {
1178
                        line: location.range.start.line,
1179
                        character: location.range.start.character
1180
                    },
1181
                    end: {
1182
                        line: location.range.end.line,
1183
                        character: location.range.end.character
1184
                    }
1185
                }
1186
            };
1187
        } else {
1188
            return location;
165✔
1189
        }
1190
    }
1191

1192
    /**
1193
     * Clone every token
1194
     */
1195
    public cloneToken<T extends Token>(token: T): T {
1196
        if (token) {
2,679✔
1197
            const result = {
2,485✔
1198
                kind: token.kind,
1199
                location: this.cloneLocation(token.location),
1200
                text: token.text,
1201
                isReserved: token.isReserved,
1202
                leadingWhitespace: token.leadingWhitespace,
1203
                leadingTrivia: token.leadingTrivia.map(x => this.cloneToken(x))
1,279✔
1204
            } as Token;
1205
            //handle those tokens that have charCode
1206
            if ('charCode' in token) {
2,485✔
1207
                (result as any).charCode = (token as any).charCode;
3✔
1208
            }
1209
            return result as T;
2,485✔
1210
        } else {
1211
            return token;
194✔
1212
        }
1213
    }
1214

1215
    /**
1216
     *  Gets the bounding range of a bunch of ranges or objects that have ranges
1217
     *  TODO: this does a full iteration of the args. If the args were guaranteed to be in range order, we could optimize this
1218
     */
1219
    public createBoundingLocation(...locatables: Array<{ location?: Location } | Location | { range?: Range } | Range | undefined>): Location | undefined {
1220
        let uri: string | undefined;
1221
        let startPosition: Position | undefined;
1222
        let endPosition: Position | undefined;
1223

1224
        for (let locatable of locatables) {
44,313✔
1225
            let range: Range;
1226
            if (!locatable) {
179,040✔
1227
                continue;
48,008✔
1228
            } else if ('location' in locatable) {
131,032✔
1229
                range = locatable.location?.range;
125,779✔
1230
                if (!uri) {
125,779✔
1231
                    uri = locatable.location?.uri;
51,092✔
1232
                }
1233
            } else if (Location.is(locatable)) {
5,253✔
1234
                range = locatable.range;
5,245✔
1235
                if (!uri) {
5,245✔
1236
                    uri = locatable.uri;
4,658✔
1237
                }
1238
            } else if ('range' in locatable) {
8!
NEW
1239
                range = locatable.range;
×
1240
            } else {
1241
                range = locatable as Range;
8✔
1242
            }
1243

1244
            //skip undefined locations or locations without a range
1245
            if (!range) {
131,032✔
1246
                continue;
3,567✔
1247
            }
1248

1249
            if (!startPosition) {
127,465✔
1250
                startPosition = range.start;
43,087✔
1251
            } else if (this.comparePosition(range.start, startPosition) < 0) {
84,378✔
1252
                startPosition = range.start;
854✔
1253
            }
1254
            if (!endPosition) {
127,465✔
1255
                endPosition = range.end;
43,087✔
1256
            } else if (this.comparePosition(range.end, endPosition) > 0) {
84,378✔
1257
                endPosition = range.end;
79,455✔
1258
            }
1259
        }
1260
        if (startPosition && endPosition) {
44,313✔
1261
            return util.createLocation(startPosition.line, startPosition.character, endPosition.line, endPosition.character, uri);
43,087✔
1262
        } else {
1263
            return undefined;
1,226✔
1264
        }
1265
    }
1266

1267
    /**
1268
     *  Gets the bounding range of a bunch of ranges or objects that have ranges
1269
     *  TODO: this does a full iteration of the args. If the args were guaranteed to be in range order, we could optimize this
1270
     */
1271
    public createBoundingRange(...locatables: Array<RangeLike>): Range | undefined {
1272
        return this.createBoundingLocation(...locatables)?.range;
511✔
1273
    }
1274

1275
    /**
1276
     * Gets the bounding range of an object that contains a bunch of tokens
1277
     * @param tokens Object with tokens in it
1278
     * @returns Range containing all the tokens
1279
     */
1280
    public createBoundingLocationFromTokens(tokens: Record<string, { location?: Location }>): Location | undefined {
1281
        let uri: string;
1282
        let startPosition: Position | undefined;
1283
        let endPosition: Position | undefined;
1284
        for (let key in tokens) {
5,051✔
1285
            let token = tokens?.[key];
17,995!
1286
            let locatableRange = token?.location?.range;
17,995✔
1287
            if (!locatableRange) {
17,995✔
1288
                continue;
5,545✔
1289
            }
1290

1291
            if (!startPosition) {
12,450✔
1292
                startPosition = locatableRange.start;
4,894✔
1293
            } else if (this.comparePosition(locatableRange.start, startPosition) < 0) {
7,556✔
1294
                startPosition = locatableRange.start;
2,079✔
1295
            }
1296
            if (!endPosition) {
12,450✔
1297
                endPosition = locatableRange.end;
4,894✔
1298
            } else if (this.comparePosition(locatableRange.end, endPosition) > 0) {
7,556✔
1299
                endPosition = locatableRange.end;
5,361✔
1300
            }
1301
            if (!uri) {
12,450✔
1302
                uri = token.location.uri;
6,051✔
1303
            }
1304
        }
1305
        if (startPosition && endPosition) {
5,051✔
1306
            return this.createLocation(startPosition.line, startPosition.character, endPosition.line, endPosition.character, uri);
4,894✔
1307
        } else {
1308
            return undefined;
157✔
1309
        }
1310
    }
1311

1312
    /**
1313
     * Create a `Position` object. Prefer this over `Position.create` for performance reasons.
1314
     */
1315
    public createPosition(line: number, character: number) {
1316
        return {
367✔
1317
            line: line,
1318
            character: character
1319
        };
1320
    }
1321

1322
    /**
1323
     * Convert a list of tokens into a string, including their leading whitespace
1324
     */
1325
    public tokensToString(tokens: Token[]) {
1326
        let result = '';
1✔
1327
        //skip iterating the final token
1328
        for (let token of tokens) {
1✔
1329
            result += token.leadingWhitespace + token.text;
16✔
1330
        }
1331
        return result;
1✔
1332
    }
1333

1334
    /**
1335
     * Convert a token into a BscType
1336
     */
1337
    public tokenToBscType(token: Token) {
1338
        // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
1339
        switch (token.kind) {
1,787,766✔
1340
            case TokenKind.Boolean:
1,791,012✔
1341
                return new BooleanType(token.text);
142✔
1342
            case TokenKind.True:
1343
            case TokenKind.False:
1344
                return BooleanType.instance;
162✔
1345
            case TokenKind.Double:
1346
                return new DoubleType(token.text);
74✔
1347
            case TokenKind.DoubleLiteral:
1348
                return DoubleType.instance;
8✔
1349
            case TokenKind.Dynamic:
1350
                return new DynamicType(token.text);
96✔
1351
            case TokenKind.Float:
1352
                return new FloatType(token.text);
300✔
1353
            case TokenKind.FloatLiteral:
1354
                return FloatType.instance;
117✔
1355
            case TokenKind.Function:
1356
                return new FunctionType(token.text);
137✔
1357
            case TokenKind.Integer:
1358
                return new IntegerType(token.text);
1,048✔
1359
            case TokenKind.IntegerLiteral:
1360
                return IntegerType.instance;
1,841✔
1361
            case TokenKind.Invalid:
1362
                return DynamicType.instance; // TODO: use InvalidType better new InvalidType(token.text);
84✔
1363
            case TokenKind.LongInteger:
1364
                return new LongIntegerType(token.text);
46✔
1365
            case TokenKind.LongIntegerLiteral:
1366
                return LongIntegerType.instance;
3✔
1367
            case TokenKind.Object:
1368
                return new ObjectType(token.text);
307✔
1369
            case TokenKind.String:
1370
                return new StringType(token.text);
1,818✔
1371
            case TokenKind.StringLiteral:
1372
            case TokenKind.TemplateStringExpressionBegin:
1373
            case TokenKind.TemplateStringExpressionEnd:
1374
            case TokenKind.TemplateStringQuasi:
1375
                return StringType.instance;
1,048✔
1376
            case TokenKind.Void:
1377
                return new VoidType(token.text);
24✔
1378
            case TokenKind.Identifier:
1379
                switch (token.text.toLowerCase()) {
1,780,488✔
1380
                    case 'boolean':
957,598!
1381
                        return new BooleanType(token.text);
221,113✔
1382
                    case 'double':
1383
                        return new DoubleType(token.text);
4✔
1384
                    case 'dynamic':
1385
                        return new DynamicType(token.text);
4✔
1386
                    case 'float':
1387
                        return new FloatType(token.text);
221,115✔
1388
                    case 'function':
NEW
1389
                        return new FunctionType(token.text);
×
1390
                    case 'integer':
1391
                        return new IntegerType(token.text);
181,068✔
1392
                    case 'invalid':
NEW
1393
                        return DynamicType.instance; // TODO: use InvalidType better new InvalidType(token.text);
×
1394
                    case 'longinteger':
1395
                        return new LongIntegerType(token.text);
4✔
1396
                    case 'object':
1397
                        return new ObjectType(token.text);
4✔
1398
                    case 'string':
1399
                        return new StringType(token.text);
334,282✔
1400
                    case 'void':
1401
                        return new VoidType(token.text);
4✔
1402
                }
1403
        }
1404
    }
1405

1406
    /**
1407
     * Deciphers the correct types for fields based on docs
1408
     * https://developer.roku.com/en-ca/docs/references/scenegraph/xml-elements/interface.md
1409
     * @param typeDescriptor the type descriptor from the docs
1410
     * @returns {BscType} the known type, or dynamic
1411
     */
1412
    public getNodeFieldType(typeDescriptor: string, lookupTable?: SymbolTable): BscType {
1413
        let typeDescriptorLower = typeDescriptor.toLowerCase().trim().replace(/\*/g, '');
1,763,672✔
1414

1415
        if (typeDescriptorLower.startsWith('as ')) {
1,763,672✔
1416
            typeDescriptorLower = typeDescriptorLower.substring(3).trim();
6,964✔
1417
        }
1418
        const nodeFilter = (new RegExp(/^\[?(.* node)/, 'i')).exec(typeDescriptorLower);
1,763,672✔
1419
        if (nodeFilter?.[1]) {
1,763,672✔
1420
            typeDescriptorLower = nodeFilter[1].trim();
36,561✔
1421
        }
1422
        const parensFilter = (new RegExp(/(.*)\(.*\)/, 'gi')).exec(typeDescriptorLower);
1,763,672✔
1423
        if (parensFilter?.[1]) {
1,763,672✔
1424
            typeDescriptorLower = parensFilter[1].trim();
3,482✔
1425
        }
1426

1427
        const bscType = this.tokenToBscType(createToken(TokenKind.Identifier, typeDescriptorLower));
1,763,672✔
1428
        if (bscType) {
1,763,672✔
1429
            return bscType;
957,562✔
1430
        }
1431

1432
        function getRect2dType() {
1433
            const rect2dType = new AssociativeArrayType();
5,227✔
1434
            rect2dType.addMember('height', {}, FloatType.instance, SymbolTypeFlag.runtime);
5,227✔
1435
            rect2dType.addMember('width', {}, FloatType.instance, SymbolTypeFlag.runtime);
5,227✔
1436
            rect2dType.addMember('x', {}, FloatType.instance, SymbolTypeFlag.runtime);
5,227✔
1437
            rect2dType.addMember('y', {}, FloatType.instance, SymbolTypeFlag.runtime);
5,227✔
1438
            return rect2dType;
5,227✔
1439
        }
1440

1441
        function getColorType() {
1442
            return unionTypeFactory([IntegerType.instance, StringType.instance]);
107,946✔
1443
        }
1444

1445
        //check for uniontypes
1446
        const multipleTypes = typeDescriptorLower.split(' or ').map(s => s.trim());
809,592✔
1447
        if (multipleTypes.length > 1) {
806,110✔
1448
            const individualTypes = multipleTypes.map(t => this.getNodeFieldType(t, lookupTable));
6,964✔
1449
            return unionTypeFactory(individualTypes);
3,482✔
1450
        }
1451

1452
        const typeIsArray = typeDescriptorLower.startsWith('array of ') || typeDescriptorLower.startsWith('roarray of ');
802,628✔
1453

1454
        if (typeIsArray) {
802,628✔
1455
            const ofSearch = ' of ';
100,978✔
1456
            const arrayPrefixLength = typeDescriptorLower.indexOf(ofSearch) + ofSearch.length;
100,978✔
1457
            let arrayOfTypeName = typeDescriptorLower.substring(arrayPrefixLength); //cut off beginnin, eg. 'array of' or 'roarray of'
100,978✔
1458
            if (arrayOfTypeName.endsWith('s')) {
100,978✔
1459
                // remove "s" in "floats", etc.
1460
                arrayOfTypeName = arrayOfTypeName.substring(0, arrayOfTypeName.length - 1);
74,863✔
1461
            }
1462
            if (arrayOfTypeName.endsWith('\'')) {
100,978✔
1463
                // remove "'" in "float's", etc.
1464
                arrayOfTypeName = arrayOfTypeName.substring(0, arrayOfTypeName.length - 1);
6,964✔
1465
            }
1466
            if (arrayOfTypeName === 'rectangle') {
100,978✔
1467
                arrayOfTypeName = 'rect2d';
1,741✔
1468
            }
1469
            let arrayType = this.getNodeFieldType(arrayOfTypeName, lookupTable);
100,978✔
1470
            return new ArrayType(arrayType);
100,978✔
1471
        } else if (typeDescriptorLower.startsWith('option ')) {
701,650✔
1472
            const actualTypeName = typeDescriptorLower.substring('option '.length); //cut off beginning 'option '
34,820✔
1473
            return this.getNodeFieldType(actualTypeName, lookupTable);
34,820✔
1474
        } else if (typeDescriptorLower.startsWith('value ')) {
666,830✔
1475
            const actualTypeName = typeDescriptorLower.substring('value '.length); //cut off beginning 'value '
13,928✔
1476
            return this.getNodeFieldType(actualTypeName, lookupTable);
13,928✔
1477
        } else if (typeDescriptorLower === 'n/a') {
652,902✔
1478
            return DynamicType.instance;
3,482✔
1479
        } else if (typeDescriptorLower === 'uri') {
649,420✔
1480
            return StringType.instance;
125,357✔
1481
        } else if (typeDescriptorLower === 'color') {
524,063✔
1482
            return getColorType();
107,945✔
1483
        } else if (typeDescriptorLower === 'vector2d' || typeDescriptorLower === 'floatarray') {
416,118✔
1484
            return new ArrayType(FloatType.instance);
40,044✔
1485
        } else if (typeDescriptorLower === 'vector2darray') {
376,074!
NEW
1486
            return new ArrayType(new ArrayType(FloatType.instance));
×
1487
        } else if (typeDescriptorLower === 'intarray') {
376,074✔
1488
            return new ArrayType(IntegerType.instance);
1✔
1489
        } else if (typeDescriptorLower === 'colorarray') {
376,073✔
1490
            return new ArrayType(getColorType());
1✔
1491
        } else if (typeDescriptorLower === 'boolarray') {
376,072!
NEW
1492
            return new ArrayType(BooleanType.instance);
×
1493
        } else if (typeDescriptorLower === 'stringarray' || typeDescriptorLower === 'strarray') {
376,072✔
1494
            return new ArrayType(StringType.instance);
1✔
1495
        } else if (typeDescriptorLower === 'int') {
376,071✔
1496
            return IntegerType.instance;
6,964✔
1497
        } else if (typeDescriptorLower === 'time') {
369,107✔
1498
            return DoubleType.instance;
33,080✔
1499
        } else if (typeDescriptorLower === 'str') {
336,027!
NEW
1500
            return StringType.instance;
×
1501
        } else if (typeDescriptorLower === 'bool') {
336,027✔
1502
            return BooleanType.instance;
1,741✔
1503
        } else if (typeDescriptorLower === 'array' || typeDescriptorLower === 'roarray') {
334,286✔
1504
            return new ArrayType();
13,929✔
1505
        } else if (typeDescriptorLower === 'assocarray' ||
320,357✔
1506
            typeDescriptorLower === 'associative array' ||
1507
            typeDescriptorLower === 'associativearray' ||
1508
            typeDescriptorLower === 'roassociativearray' ||
1509
            typeDescriptorLower.startsWith('associative array of') ||
1510
            typeDescriptorLower.startsWith('associativearray of') ||
1511
            typeDescriptorLower.startsWith('roassociativearray of')
1512
        ) {
1513
            return new AssociativeArrayType();
60,936✔
1514
        } else if (typeDescriptorLower === 'node') {
259,421✔
1515
            return ComponentType.instance;
15,670✔
1516
        } else if (typeDescriptorLower === 'nodearray') {
243,751✔
1517
            return new ArrayType(ComponentType.instance);
1✔
1518
        } else if (typeDescriptorLower === 'rect2d') {
243,750✔
1519
            return getRect2dType();
5,225✔
1520
        } else if (typeDescriptorLower === 'rect2darray') {
238,525✔
1521
            return new ArrayType(getRect2dType());
2✔
1522
        } else if (typeDescriptorLower === 'font') {
238,523✔
1523
            return this.getNodeFieldType('roSGNodeFont', lookupTable);
38,304✔
1524
        } else if (typeDescriptorLower === 'contentnode') {
200,219✔
1525
            return this.getNodeFieldType('roSGNodeContentNode', lookupTable);
34,820✔
1526
        } else if (typeDescriptorLower.endsWith(' node')) {
165,399✔
1527
            return this.getNodeFieldType('roSgNode' + typeDescriptorLower.substring(0, typeDescriptorLower.length - 5), lookupTable);
34,820✔
1528
        } else if (lookupTable) {
130,579!
1529
            //try doing a lookup
1530
            return lookupTable.getSymbolType(typeDescriptorLower, {
130,579✔
1531
                flags: SymbolTypeFlag.typetime,
1532
                fullName: typeDescriptor,
1533
                tableProvider: () => lookupTable
2✔
1534
            });
1535
        }
1536

NEW
1537
        return DynamicType.instance;
×
1538
    }
1539

1540
    /**
1541
     * Return the type of the result of a binary operator
1542
     * Note: compound assignments (eg. +=) internally use a binary expression, so that's why TokenKind.PlusEqual, etc. are here too
1543
     */
1544
    public binaryOperatorResultType(leftType: BscType, operator: Token, rightType: BscType): BscType {
1545
        if ((isAnyReferenceType(leftType) && !leftType.isResolvable()) ||
520✔
1546
            (isAnyReferenceType(rightType) && !rightType.isResolvable())) {
1547
            return new BinaryOperatorReferenceType(leftType, operator, rightType, (lhs, op, rhs) => {
29✔
1548
                return this.binaryOperatorResultType(lhs, op, rhs);
2✔
1549
            });
1550
        }
1551
        if (isEnumMemberType(leftType)) {
491✔
1552
            leftType = leftType.underlyingType;
9✔
1553
        }
1554
        if (isEnumMemberType(rightType)) {
491✔
1555
            rightType = rightType.underlyingType;
8✔
1556
        }
1557
        let hasDouble = isDoubleType(leftType) || isDoubleType(rightType);
491✔
1558
        let hasFloat = isFloatType(leftType) || isFloatType(rightType);
491✔
1559
        let hasLongInteger = isLongIntegerType(leftType) || isLongIntegerType(rightType);
491✔
1560
        let hasInvalid = isInvalidType(leftType) || isInvalidType(rightType);
491✔
1561
        let hasDynamic = isDynamicType(leftType) || isDynamicType(rightType);
491✔
1562
        let bothNumbers = isNumberType(leftType) && isNumberType(rightType);
491✔
1563
        let bothStrings = isStringType(leftType) && isStringType(rightType);
491✔
1564
        let eitherBooleanOrNum = (isNumberType(leftType) || isBooleanType(leftType)) && (isNumberType(rightType) || isBooleanType(rightType));
491✔
1565

1566
        // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
1567
        switch (operator.kind) {
491✔
1568
            // Math operators
1569
            case TokenKind.Plus:
1,475✔
1570
            case TokenKind.PlusEqual:
1571
                if (bothStrings) {
213✔
1572
                    // "string" + "string" is the only binary expression allowed with strings
1573
                    return StringType.instance;
121✔
1574
                }
1575
            // eslint-disable-next-line no-fallthrough
1576
            case TokenKind.Minus:
1577
            case TokenKind.MinusEqual:
1578
            case TokenKind.Star:
1579
            case TokenKind.StarEqual:
1580
            case TokenKind.Mod:
1581
                if (bothNumbers) {
156✔
1582
                    if (hasDouble) {
127✔
1583
                        return DoubleType.instance;
5✔
1584
                    } else if (hasFloat) {
122✔
1585
                        return FloatType.instance;
23✔
1586

1587
                    } else if (hasLongInteger) {
99✔
1588
                        return LongIntegerType.instance;
4✔
1589
                    }
1590
                    return IntegerType.instance;
95✔
1591
                }
1592
                break;
29✔
1593
            case TokenKind.Forwardslash:
1594
            case TokenKind.ForwardslashEqual:
1595
                if (bothNumbers) {
10✔
1596
                    if (hasDouble) {
8✔
1597
                        return DoubleType.instance;
1✔
1598
                    } else if (hasFloat) {
7✔
1599
                        return FloatType.instance;
1✔
1600

1601
                    } else if (hasLongInteger) {
6✔
1602
                        return LongIntegerType.instance;
1✔
1603
                    }
1604
                    return FloatType.instance;
5✔
1605
                }
1606
                break;
2✔
1607
            case TokenKind.Backslash:
1608
            case TokenKind.BackslashEqual:
1609
                if (bothNumbers) {
6✔
1610
                    if (hasLongInteger) {
4!
NEW
1611
                        return LongIntegerType.instance;
×
1612
                    }
1613
                    return IntegerType.instance;
4✔
1614
                }
1615
                break;
2✔
1616
            case TokenKind.Caret:
1617
                if (bothNumbers) {
16✔
1618
                    if (hasDouble || hasLongInteger) {
14✔
1619
                        return DoubleType.instance;
2✔
1620
                    } else if (hasFloat) {
12✔
1621
                        return FloatType.instance;
1✔
1622
                    }
1623
                    return IntegerType.instance;
11✔
1624
                }
1625
                break;
2✔
1626
            // Bitshift operators
1627
            case TokenKind.LeftShift:
1628
            case TokenKind.LeftShiftEqual:
1629
            case TokenKind.RightShift:
1630
            case TokenKind.RightShiftEqual:
1631
                if (bothNumbers) {
18✔
1632
                    if (hasLongInteger) {
14✔
1633
                        return LongIntegerType.instance;
2✔
1634
                    }
1635
                    // Bitshifts are allowed with non-integer numerics
1636
                    // but will always truncate to ints
1637
                    return IntegerType.instance;
12✔
1638
                }
1639
                break;
4✔
1640
            // Comparison operators
1641
            // All comparison operators result in boolean
1642
            case TokenKind.Equal:
1643
            case TokenKind.LessGreater:
1644
                // = and <> can accept invalid / dynamic
1645
                if (hasDynamic || hasInvalid || bothStrings || eitherBooleanOrNum) {
83✔
1646
                    return BooleanType.instance;
80✔
1647
                }
1648
                break;
3✔
1649
            case TokenKind.Greater:
1650
            case TokenKind.Less:
1651
            case TokenKind.GreaterEqual:
1652
            case TokenKind.LessEqual:
1653
                if (bothStrings || bothNumbers) {
23✔
1654
                    return BooleanType.instance;
11✔
1655
                }
1656
                break;
12✔
1657
            // Logical or bitwise operators
1658
            case TokenKind.Or:
1659
            case TokenKind.And:
1660
                if (bothNumbers) {
58✔
1661
                    // "and"/"or" represent bitwise operators
1662
                    if (hasLongInteger && !hasDouble && !hasFloat) {
12✔
1663
                        // 2 long ints or long int and int
1664
                        return LongIntegerType.instance;
1✔
1665
                    }
1666
                    return IntegerType.instance;
11✔
1667
                } else if (eitherBooleanOrNum) {
46✔
1668
                    // "and"/"or" represent logical operators
1669
                    return BooleanType.instance;
39✔
1670
                }
1671
                break;
7✔
1672
        }
1673
        return DynamicType.instance;
61✔
1674
    }
1675

1676
    /**
1677
     * Return the type of the result of a binary operator
1678
     */
1679
    public unaryOperatorResultType(operator: Token, exprType: BscType): BscType {
1680
        // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
1681
        switch (operator.kind) {
86✔
1682
            // Math operators
1683
            case TokenKind.Minus:
82✔
1684
                if (isNumberType(exprType)) {
59✔
1685
                    // a negative number will be the same type, eg, double->double, int->int, etc.
1686
                    return exprType;
49✔
1687
                }
1688
                break;
10✔
1689
            case TokenKind.Not:
1690
                if (isBooleanType(exprType)) {
23✔
1691
                    return BooleanType.instance;
10✔
1692
                } else if (isNumberType(exprType)) {
13✔
1693
                    //numbers can be "notted"
1694
                    // by default they go to ints, except longints, which stay that way
1695
                    if (isLongIntegerType(exprType)) {
10✔
1696
                        return LongIntegerType.instance;
1✔
1697
                    }
1698
                    return IntegerType.instance;
9✔
1699
                }
1700
                break;
3✔
1701
        }
1702
        return DynamicType.instance;
17✔
1703
    }
1704

1705
    /**
1706
     * Get the extension for the given file path. Basically the part after the final dot, except for
1707
     * `d.bs` which is treated as single extension
1708
     * @returns the file extension (i.e. ".d.bs", ".bs", ".brs", ".xml", ".jpg", etc...)
1709
     */
1710
    public getExtension(filePath: string) {
1711
        filePath = filePath.toLowerCase();
2,579✔
1712
        if (filePath.endsWith('.d.bs')) {
2,579✔
1713
            return '.d.bs';
33✔
1714
        } else {
1715
            return path.extname(filePath).toLowerCase();
2,546✔
1716
        }
1717
    }
1718

1719
    /**
1720
     * Load and return the list of plugins
1721
     */
1722
    public loadPlugins(cwd: string, pathOrModules: string[], onError?: (pathOrModule: string, err: Error) => void): CompilerPlugin[] {
1723
        const logger = createLogger();
53✔
1724
        return pathOrModules.reduce<CompilerPlugin[]>((acc, pathOrModule) => {
53✔
1725
            if (typeof pathOrModule === 'string') {
6!
1726
                try {
6✔
1727
                    const loaded = requireRelative(pathOrModule, cwd);
6✔
1728
                    const theExport: CompilerPlugin | CompilerPluginFactory = loaded.default ? loaded.default : loaded;
6✔
1729

1730
                    let plugin: CompilerPlugin | undefined;
1731

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

1737
                        // the official plugin format is a factory function that returns a new instance of a plugin.
1738
                    } else if (typeof theExport === 'function') {
4!
1739
                        plugin = theExport();
4✔
1740
                    } else {
1741
                        //this should never happen; somehow an invalid plugin has made it into here
1742
                        throw new Error(`TILT: Encountered an invalid plugin: ${String(plugin)}`);
×
1743
                    }
1744

1745
                    if (!plugin.name) {
6!
1746
                        plugin.name = pathOrModule;
×
1747
                    }
1748
                    acc.push(plugin);
6✔
1749
                } catch (err: any) {
1750
                    if (onError) {
×
1751
                        onError(pathOrModule, err);
×
1752
                    } else {
1753
                        throw err;
×
1754
                    }
1755
                }
1756
            }
1757
            return acc;
6✔
1758
        }, []);
1759
    }
1760

1761
    /**
1762
     * Gathers expressions, variables, and unique names from an expression.
1763
     * This is mostly used for the ternary expression
1764
     */
1765
    public getExpressionInfo(expression: Expression, file: BrsFile): ExpressionInfo {
1766
        const expressions = [expression];
78✔
1767
        const variableExpressions = [] as VariableExpression[];
78✔
1768
        const uniqueVarNames = new Set<string>();
78✔
1769

1770
        function expressionWalker(expression) {
1771
            if (isExpression(expression)) {
120✔
1772
                expressions.push(expression);
116✔
1773
            }
1774
            if (isVariableExpression(expression)) {
120✔
1775
                variableExpressions.push(expression);
30✔
1776
                uniqueVarNames.add(expression.tokens.name.text);
30✔
1777
            }
1778
        }
1779

1780
        // Collect all expressions. Most of these expressions are fairly small so this should be quick!
1781
        // This should only be called during transpile time and only when we actually need it.
1782
        expression?.walk(expressionWalker, {
78✔
1783
            walkMode: WalkMode.visitExpressions
1784
        });
1785

1786
        //handle the expression itself (for situations when expression is a VariableExpression)
1787
        expressionWalker(expression);
78✔
1788

1789
        const scope = file.program.getFirstScopeForFile(file);
78✔
1790
        let filteredVarNames = [...uniqueVarNames];
78✔
1791
        if (scope) {
78!
1792
            filteredVarNames = filteredVarNames.filter((varName: string) => {
78✔
1793
                const varNameLower = varName.toLowerCase();
28✔
1794
                // TODO: include namespaces in this filter
1795
                return !scope.getEnumMap().has(varNameLower) &&
28✔
1796
                    !scope.getConstMap().has(varNameLower);
1797
            });
1798
        }
1799

1800
        return { expressions: expressions, varExpressions: variableExpressions, uniqueVarNames: filteredVarNames };
78✔
1801
    }
1802

1803

1804
    public concatAnnotationLeadingTrivia(stmt: Statement): Token[] {
1805
        return [...(stmt.annotations?.map(anno => anno.leadingTrivia ?? []).flat() ?? []), ...stmt.leadingTrivia];
168!
1806
    }
1807

1808
    /**
1809
     * Create a SourceNode that maps every line to itself. Useful for creating maps for files
1810
     * that haven't changed at all, but we still need the map
1811
     */
1812
    public simpleMap(source: string, src: string) {
1813
        //create a source map from the original source code
1814
        let chunks = [] as (SourceNode | string)[];
5✔
1815
        let lines = src.split(/\r?\n/g);
5✔
1816
        for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
5✔
1817
            let line = lines[lineIndex];
19✔
1818
            chunks.push(
19✔
1819
                lineIndex > 0 ? '\n' : '',
19✔
1820
                new SourceNode(lineIndex + 1, 0, source, line)
1821
            );
1822
        }
1823
        return new SourceNode(null, null, source, chunks);
5✔
1824
    }
1825

1826
    /**
1827
     * Converts a path into a standardized format (drive letter to lower, remove extra slashes, use single slash type, resolve relative parts, etc...)
1828
     */
1829
    public standardizePath(thePath: string) {
1830
        return util.driveLetterToLower(
11,225✔
1831
            rokuDeployStandardizePath(thePath)
1832
        );
1833
    }
1834

1835
    /**
1836
     * Given a Diagnostic or BsDiagnostic, return a deep clone of the diagnostic.
1837
     * @param diagnostic the diagnostic to clone
1838
     * @param relatedInformationFallbackLocation a default location to use for all `relatedInformation` entries that are missing a location
1839
     */
1840
    public toDiagnostic(diagnostic: Diagnostic | BsDiagnostic, relatedInformationFallbackLocation: string): Diagnostic {
1841
        let relatedInformation = diagnostic.relatedInformation ?? [];
15✔
1842
        if (relatedInformation.length > MAX_RELATED_INFOS_COUNT) {
15!
NEW
1843
            const relatedInfoLength = relatedInformation.length;
×
NEW
1844
            relatedInformation = relatedInformation.slice(0, MAX_RELATED_INFOS_COUNT);
×
NEW
1845
            relatedInformation.push({
×
1846
                message: `...and ${relatedInfoLength - MAX_RELATED_INFOS_COUNT} more`,
1847
                location: util.createLocationFromRange('   ', util.createRange(0, 0, 0, 0))
1848
            });
1849
        }
1850

1851
        const range = (diagnostic as BsDiagnostic).location?.range ??
15✔
1852
            (diagnostic as Diagnostic).range;
1853

1854
        let result = {
15✔
1855
            severity: diagnostic.severity,
1856
            range: range,
1857
            message: diagnostic.message,
1858
            relatedInformation: relatedInformation.map(x => {
1859

1860
                //clone related information just in case a plugin added circular ref info here
1861
                const clone = { ...x };
4✔
1862
                if (!clone.location) {
4✔
1863
                    // use the fallback location if available
1864
                    if (relatedInformationFallbackLocation) {
2✔
1865
                        clone.location = util.createLocationFromRange(relatedInformationFallbackLocation, range);
1✔
1866
                    } else {
1867
                        //remove this related information so it doesn't bring crash the language server
1868
                        return undefined;
1✔
1869
                    }
1870
                }
1871
                return clone;
3✔
1872
                //filter out null relatedInformation items
1873
            }).filter((x): x is DiagnosticRelatedInformation => Boolean(x)),
4✔
1874
            code: diagnostic.code,
1875
            source: 'brs'
1876
        } as Diagnostic;
1877
        if (diagnostic?.tags?.length > 0) {
15!
NEW
1878
            result.tags = diagnostic.tags;
×
1879
        }
1880
        return result;
15✔
1881
    }
1882

1883
    /**
1884
     * Get the first locatable item found at the specified position
1885
     * @param locatables an array of items that have a `range` property
1886
     * @param position the position that the locatable must contain
1887
     */
1888
    public getFirstLocatableAt(locatables: Locatable[], position: Position) {
1889
        for (let token of locatables) {
×
NEW
1890
            if (util.rangeContains(token.location?.range, position)) {
×
1891
                return token;
×
1892
            }
1893
        }
1894
    }
1895

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

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

1941
    /**
1942
     * Split the given text and return ranges for each chunk.
1943
     * Only works for single-line strings
1944
     */
1945
    public splitGetRange(separator: string, text: string, range: Range) {
1946
        const chunks = text.split(separator);
3✔
1947
        const result = [] as Array<{ text: string; range: Range }>;
3✔
1948
        let offset = 0;
3✔
1949
        for (let chunk of chunks) {
3✔
1950
            //only keep nonzero chunks
1951
            if (chunk.length > 0) {
8✔
1952
                result.push({
7✔
1953
                    text: chunk,
1954
                    range: this.createRange(
1955
                        range.start.line,
1956
                        range.start.character + offset,
1957
                        range.end.line,
1958
                        range.start.character + offset + chunk.length
1959
                    )
1960
                });
1961
            }
1962
            offset += chunk.length + separator.length;
8✔
1963
        }
1964
        return result;
3✔
1965
    }
1966

1967
    /**
1968
     * Wrap the given code in a markdown code fence (with the language)
1969
     */
1970
    public mdFence(code: string, language = '') {
×
1971
        return '```' + language + '\n' + code + '\n```';
120✔
1972
    }
1973

1974
    /**
1975
     * Gets each part of the dotted get.
1976
     * @param node any ast expression
1977
     * @returns an array of the parts of the dotted get. If not fully a dotted get, then returns undefined
1978
     */
1979
    public getAllDottedGetParts(node: AstNode): Identifier[] | undefined {
1980
        //this is a hot function and has been optimized. Don't rewrite unless necessary
1981
        const parts: Identifier[] = [];
12,060✔
1982
        let nextPart = node;
12,060✔
1983
        loop: while (nextPart) {
12,060✔
1984
            switch (nextPart?.kind) {
18,055!
1985
                case AstNodeKind.AssignmentStatement:
18,055!
1986
                    return [(node as AssignmentStatement).tokens.name];
9✔
1987
                case AstNodeKind.DottedGetExpression:
1988
                    parts.push((nextPart as DottedGetExpression)?.tokens.name);
5,924!
1989
                    nextPart = (nextPart as DottedGetExpression).obj;
5,924✔
1990
                    continue;
5,924✔
1991
                case AstNodeKind.CallExpression:
1992
                    nextPart = (nextPart as CallExpression).callee;
39✔
1993
                    continue;
39✔
1994
                case AstNodeKind.TypeExpression:
NEW
1995
                    nextPart = (nextPart as TypeExpression).expression;
×
NEW
1996
                    continue;
×
1997
                case AstNodeKind.VariableExpression:
1998
                    parts.push((nextPart as VariableExpression)?.tokens.name);
11,950!
1999
                    break loop;
11,950✔
2000
                case AstNodeKind.LiteralExpression:
2001
                    parts.push((nextPart as LiteralExpression)?.tokens.value as Identifier);
4!
2002
                    break loop;
4✔
2003
                case AstNodeKind.IndexedGetExpression:
2004
                    nextPart = (nextPart as unknown as IndexedGetExpression).obj;
35✔
2005
                    continue;
35✔
2006
                case AstNodeKind.FunctionParameterExpression:
2007
                    return [(nextPart as FunctionParameterExpression).tokens.name];
6✔
2008
                case AstNodeKind.GroupingExpression:
2009
                    parts.push(createIdentifier('()', nextPart.location));
6✔
2010
                    break loop;
6✔
2011
                default:
2012
                    //we found a non-DottedGet expression, so return because this whole operation is invalid.
2013
                    return undefined;
82✔
2014
            }
2015
        }
2016
        return parts.reverse();
11,963✔
2017
    }
2018

2019
    /**
2020
     * Given an expression, return all the DottedGet name parts as a string.
2021
     * Mostly used to convert namespaced item full names to a strings
2022
     */
2023
    public getAllDottedGetPartsAsString(node: Expression | Statement, parseMode = ParseMode.BrighterScript): string {
1,944✔
2024
        //this is a hot function and has been optimized. Don't rewrite unless necessary
2025
        /* eslint-disable no-var */
2026
        var sep = parseMode === ParseMode.BrighterScript ? '.' : '_';
10,559✔
2027
        const parts = this.getAllDottedGetParts(node) ?? [];
10,559✔
2028
        var result = parts[0]?.text;
10,559✔
2029
        for (var i = 1; i < parts.length; i++) {
10,559✔
2030
            result += sep + parts[i].text;
4,912✔
2031
        }
2032
        return result;
10,559✔
2033
        /* eslint-enable no-var */
2034
    }
2035

2036
    public stringJoin(strings: string[], separator: string) {
2037
        // eslint-disable-next-line no-var
NEW
2038
        var result = strings[0] ?? '';
×
2039
        // eslint-disable-next-line no-var
NEW
2040
        for (var i = 1; i < strings.length; i++) {
×
NEW
2041
            result += separator + strings[i];
×
2042
        }
NEW
2043
        return result;
×
2044
    }
2045

2046
    /**
2047
     * Break an expression into each part.
2048
     */
2049
    public splitExpression(expression: Expression) {
2050
        const parts: Expression[] = [expression];
10,108✔
2051
        let nextPart = expression;
10,108✔
2052
        while (nextPart) {
10,108✔
2053
            if (isDottedGetExpression(nextPart) || isIndexedGetExpression(nextPart) || isXmlAttributeGetExpression(nextPart)) {
12,691✔
2054
                nextPart = nextPart.obj;
1,008✔
2055

2056
            } else if (isCallExpression(nextPart) || isCallfuncExpression(nextPart)) {
11,683✔
2057
                nextPart = nextPart.callee;
1,575✔
2058

2059
            } else if (isTypeExpression(nextPart)) {
10,108!
2060
                nextPart = nextPart.expression;
×
2061
            } else {
2062
                break;
10,108✔
2063
            }
2064
            parts.unshift(nextPart);
2,583✔
2065
        }
2066
        return parts;
10,108✔
2067
    }
2068

2069
    /**
2070
     * Break an expression into each part, and return any VariableExpression or DottedGet expresisons from left-to-right.
2071
     */
2072
    public getDottedGetPath(expression: Expression): [VariableExpression, ...DottedGetExpression[]] {
UNCOV
2073
        let parts: Expression[] = [];
×
UNCOV
2074
        let nextPart = expression;
×
NEW
2075
        loop: while (nextPart) {
×
NEW
2076
            switch (nextPart?.kind) {
×
2077
                case AstNodeKind.DottedGetExpression:
×
NEW
2078
                    parts.push(nextPart);
×
NEW
2079
                    nextPart = (nextPart as DottedGetExpression).obj;
×
NEW
2080
                    continue;
×
2081
                case AstNodeKind.IndexedGetExpression:
2082
                case AstNodeKind.XmlAttributeGetExpression:
NEW
2083
                    nextPart = (nextPart as IndexedGetExpression | XmlAttributeGetExpression).obj;
×
NEW
2084
                    parts = [];
×
NEW
2085
                    continue;
×
2086
                case AstNodeKind.CallExpression:
2087
                case AstNodeKind.CallfuncExpression:
NEW
2088
                    nextPart = (nextPart as CallExpression | CallfuncExpression).callee;
×
NEW
2089
                    parts = [];
×
NEW
2090
                    continue;
×
2091
                case AstNodeKind.NewExpression:
NEW
2092
                    nextPart = (nextPart as NewExpression).call.callee;
×
NEW
2093
                    parts = [];
×
NEW
2094
                    continue;
×
2095
                case AstNodeKind.TypeExpression:
NEW
2096
                    nextPart = (nextPart as TypeExpression).expression;
×
NEW
2097
                    continue;
×
2098
                case AstNodeKind.VariableExpression:
NEW
2099
                    parts.push(nextPart);
×
NEW
2100
                    break loop;
×
2101
                default:
NEW
2102
                    return [] as any;
×
2103
            }
2104
        }
NEW
2105
        return parts.reverse() as any;
×
2106
    }
2107

2108
    /**
2109
     * Returns an integer if valid, or undefined. Eliminates checking for NaN
2110
     */
2111
    public parseInt(value: any) {
2112
        const result = parseInt(value);
34✔
2113
        if (!isNaN(result)) {
34✔
2114
            return result;
29✔
2115
        } else {
2116
            return undefined;
5✔
2117
        }
2118
    }
2119

2120
    /**
2121
     * Converts a range to a string in the format 1:2-3:4
2122
     */
2123
    public rangeToString(range: Range) {
2124
        return `${range?.start?.line}:${range?.start?.character}-${range?.end?.line}:${range?.end?.character}`;
1,849✔
2125
    }
2126

2127
    public validateTooDeepFile(file: (BrsFile | XmlFile)) {
2128
        //find any files nested too deep
2129
        let destPath = file?.destPath?.toString();
1,912!
2130
        let rootFolder = destPath?.replace(/^pkg:/, '').split(/[\\\/]/)[0].toLowerCase();
1,912!
2131

2132
        if (isBrsFile(file) && rootFolder !== 'source') {
1,912✔
2133
            return;
277✔
2134
        }
2135

2136
        if (isXmlFile(file) && rootFolder !== 'components') {
1,635!
2137
            return;
×
2138
        }
2139

2140
        let fileDepth = this.getParentDirectoryCount(destPath);
1,635✔
2141
        if (fileDepth >= 8) {
1,635✔
2142
            file.program?.diagnostics.register({
3!
2143
                ...DiagnosticMessages.detectedTooDeepFileSource(fileDepth),
2144
                location: util.createLocationFromFileRange(file, this.createRange(0, 0, 0, Number.MAX_VALUE))
2145
            });
2146
        }
2147
    }
2148

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

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

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

2200
                let typeString = chainItem.type?.toString();
1,926✔
2201
                let typeToFindStringFor = chainItem.type;
1,926✔
2202
                while (typeToFindStringFor) {
1,926✔
2203
                    if (isUnionType(chainItem.type)) {
1,922✔
2204
                        typeString = `(${typeToFindStringFor.toString()})`;
7✔
2205
                        break;
7✔
2206
                    } else if (isCallableType(typeToFindStringFor)) {
1,915✔
2207
                        if (isTypedFunctionType(typeToFindStringFor) && i < typeChain.length - 1) {
37✔
2208
                            typeToFindStringFor = typeToFindStringFor.returnType;
10✔
2209
                        } else {
2210
                            typeString = 'function';
27✔
2211
                            break;
27✔
2212
                        }
2213
                        parentTypeName = previousTypeName;
10✔
2214
                    } else if (isNamespaceType(typeToFindStringFor) && parentTypeName) {
1,878✔
2215
                        const chainItemTypeName = typeToFindStringFor.toString();
334✔
2216
                        typeString = parentTypeName + '.' + chainItemTypeName;
334✔
2217
                        if (chainItemTypeName.toLowerCase().startsWith(parentTypeName.toLowerCase())) {
334!
2218
                            // the following namespace already knows...
2219
                            typeString = chainItemTypeName;
334✔
2220
                        }
2221
                        break;
334✔
2222
                    } else {
2223
                        typeString = typeToFindStringFor?.toString();
1,544!
2224
                        break;
1,544✔
2225
                    }
2226
                }
2227

2228
                previousTypeName = typeString ?? '';
1,926✔
2229
                itemName = chainItem.name;
1,926✔
2230
                astNode = chainItem.astNode;
1,926✔
2231
                containsDynamic = containsDynamic || (isDynamicType(chainItem.type) && !isAnyReferenceType(chainItem.type));
1,926✔
2232
                if (!chainItem.isResolved) {
1,926✔
2233
                    errorLocation = chainItem.location;
922✔
2234
                    continueResolvingAllItems = false;
922✔
2235
                }
2236
            }
2237
        }
2238
        return {
1,064✔
2239
            itemName: itemName,
2240
            itemTypeKind: itemTypeKind,
2241
            itemParentTypeName: parentTypeName,
2242
            itemParentTypeKind: parentTypeKind,
2243
            fullNameOfItem: fullErrorName,
2244
            fullChainName: fullChainName,
2245
            location: errorLocation,
2246
            containsDynamic: containsDynamic,
2247
            astNode: astNode
2248
        };
2249
    }
2250

2251

2252
    public isInTypeExpression(expression: AstNode): boolean {
2253
        //TODO: this is much faster than node.findAncestor(), but may need to be updated for "complicated" type expressions
2254
        if (isTypeExpression(expression) ||
15,234✔
2255
            isTypeExpression(expression.parent) ||
2256
            isTypedArrayExpression(expression) ||
2257
            isTypedArrayExpression(expression.parent)) {
2258
            return true;
4,398✔
2259
        }
2260
        if (isBinaryExpression(expression.parent)) {
10,836✔
2261
            let currentExpr: AstNode = expression.parent;
2,165✔
2262
            while (isBinaryExpression(currentExpr) && currentExpr.tokens.operator.kind === TokenKind.Or) {
2,165✔
2263
                currentExpr = currentExpr.parent;
118✔
2264
            }
2265
            return isTypeExpression(currentExpr) || isTypedArrayExpression(currentExpr);
2,165✔
2266
        }
2267
        return false;
8,671✔
2268
    }
2269

2270
    public hasAnyRequiredSymbolChanged(requiredSymbols: UnresolvedSymbol[], changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
2271
        if (!requiredSymbols || !changedSymbols) {
1,805!
NEW
2272
            return false;
×
2273
        }
2274
        const runTimeChanges = changedSymbols.get(SymbolTypeFlag.runtime);
1,805✔
2275
        const typeTimeChanges = changedSymbols.get(SymbolTypeFlag.typetime);
1,805✔
2276

2277
        for (const symbol of requiredSymbols) {
1,805✔
2278
            if (this.setContainsUnresolvedSymbol(runTimeChanges, symbol) || this.setContainsUnresolvedSymbol(typeTimeChanges, symbol)) {
637✔
2279
                return true;
299✔
2280
            }
2281
        }
2282

2283
        return false;
1,506✔
2284
    }
2285

2286
    public setContainsUnresolvedSymbol(symbolLowerNameSet: Set<string>, symbol: UnresolvedSymbol) {
2287
        if (!symbolLowerNameSet || symbolLowerNameSet.size === 0) {
986✔
2288
            return false;
361✔
2289
        }
2290

2291
        for (const possibleNameLower of symbol.lookups) {
625✔
2292
            if (symbolLowerNameSet.has(possibleNameLower)) {
2,241✔
2293
                return true;
299✔
2294
            }
2295
        }
2296
        return false;
326✔
2297
    }
2298

2299
    public truncate<T>(options: {
2300
        leadingText: string;
2301
        items: T[];
2302
        trailingText?: string;
2303
        maxLength: number;
2304
        itemSeparator?: string;
2305
        partBuilder?: (item: T) => string;
2306
    }): string {
2307
        let leadingText = options.leadingText;
19✔
2308
        let items = options?.items ?? [];
19!
2309
        let trailingText = options?.trailingText ?? '';
19!
2310
        let maxLength = options?.maxLength ?? 160;
19!
2311
        let itemSeparator = options?.itemSeparator ?? ', ';
19!
2312
        let partBuilder = options?.partBuilder ?? ((x) => x.toString());
19!
2313

2314
        let parts = [];
19✔
2315
        let length = leadingText.length + (trailingText?.length ?? 0);
19!
2316

2317
        //calculate the max number of items we could fit in the given space
2318
        for (let i = 0; i < items.length; i++) {
19✔
2319
            let part = partBuilder(items[i]);
91✔
2320
            if (i > 0) {
91✔
2321
                part = itemSeparator + part;
72✔
2322
            }
2323
            parts.push(part);
91✔
2324
            length += part.length;
91✔
2325
            //exit the loop if we've maxed out our length
2326
            if (length >= maxLength) {
91✔
2327
                break;
6✔
2328
            }
2329
        }
2330
        let message: string;
2331
        //we have enough space to include all the parts
2332
        if (parts.length >= items.length) {
19✔
2333
            message = leadingText + parts.join('') + trailingText;
13✔
2334

2335
            //we require truncation
2336
        } else {
2337
            //account for truncation message length including max possible "more" items digits, trailing text length, and the separator between last item and trailing text
2338
            length = leadingText.length + `...and ${items.length} more`.length + itemSeparator.length + (trailingText?.length ?? 0);
6!
2339
            message = leadingText;
6✔
2340
            for (let i = 0; i < parts.length; i++) {
6✔
2341
                //always include at least 2 items. if this part would overflow the max, then skip it and finalize the message
2342
                if (i > 1 && length + parts[i].length > maxLength) {
47✔
2343
                    message += itemSeparator + `...and ${items.length - i} more` + trailingText;
6✔
2344
                    return message;
6✔
2345
                } else {
2346
                    message += parts[i];
41✔
2347
                    length += parts[i].length;
41✔
2348
                }
2349
            }
2350
        }
2351
        return message;
13✔
2352
    }
2353

2354
    public getAstNodeFriendlyName(node: AstNode) {
2355
        return node?.kind.replace(/Statement|Expression/g, '');
235!
2356
    }
2357

2358

2359
    public hasLeadingComments(input: Token | AstNode) {
2360
        const leadingTrivia = isToken(input) ? input?.leadingTrivia : input?.leadingTrivia ?? [];
6,962!
2361
        return !!leadingTrivia.find(t => t.kind === TokenKind.Comment);
14,244✔
2362
    }
2363

2364
    public getLeadingComments(input: Token | AstNode) {
2365
        const leadingTrivia = isToken(input) ? input?.leadingTrivia : input?.leadingTrivia ?? [];
11,036!
2366
        return leadingTrivia.filter(t => t.kind === TokenKind.Comment);
33,961✔
2367
    }
2368

2369
    public isLeadingCommentOnSameLine(line: RangeLike, input: Token | AstNode) {
2370
        const leadingCommentRange = this.getLeadingComments(input)?.[0];
10,229!
2371
        if (leadingCommentRange) {
10,229✔
2372
            return this.linesTouch(line, leadingCommentRange?.location);
1,500!
2373
        }
2374
        return false;
8,729✔
2375
    }
2376

2377
    public isClassUsedAsFunction(potentialClassType: BscType, expression: Expression, options: GetTypeOptions) {
2378
        // eslint-disable-next-line no-bitwise
2379
        if ((options?.flags ?? 0) & SymbolTypeFlag.runtime &&
22,746!
2380
            isClassType(potentialClassType) &&
2381
            !options.isExistenceTest &&
2382
            potentialClassType.name?.toLowerCase() === this.getAllDottedGetPartsAsString(expression)?.toLowerCase() &&
6,210✔
2383
            !expression?.findAncestor(isNewExpression)) {
1,224✔
2384
            return true;
32✔
2385
        }
2386
        return false;
22,714✔
2387
    }
2388

2389
    public getSpecialCaseCallExpressionReturnType(callExpr: CallExpression) {
2390
        if (isVariableExpression(callExpr.callee) && callExpr.callee.tokens.name.text.toLowerCase() === 'createobject') {
640✔
2391
            const componentName = isLiteralString(callExpr.args[0]) ? callExpr.args[0].tokens.value?.text?.replace(/"/g, '') : '';
116!
2392
            const nodeType = componentName.toLowerCase() === 'rosgnode' && isLiteralString(callExpr.args[1]) ? callExpr.args[1].tokens.value?.text?.replace(/"/g, '') : '';
116!
2393
            if (componentName?.toLowerCase().startsWith('ro')) {
116!
2394
                const fullName = componentName + nodeType;
102✔
2395
                const data = {};
102✔
2396
                const symbolTable = callExpr.getSymbolTable();
102✔
2397
                const foundType = symbolTable.getSymbolType(fullName, {
102✔
2398
                    flags: SymbolTypeFlag.typetime,
2399
                    data: data,
2400
                    tableProvider: () => callExpr?.getSymbolTable(),
117!
2401
                    fullName: fullName
2402
                });
2403
                if (foundType) {
102!
2404
                    return foundType;
102✔
2405
                }
2406
            }
2407
        }
2408
    }
2409

2410
    public symbolComesFromSameNode(symbolName: string, definingNode: AstNode, symbolTable: SymbolTable) {
2411
        let nsData: ExtraSymbolData = {};
588✔
2412
        let foundType = symbolTable?.getSymbolType(symbolName, { flags: SymbolTypeFlag.runtime, data: nsData });
588!
2413
        if (foundType && definingNode === nsData?.definingNode) {
588!
2414
            return true;
216✔
2415
        }
2416
        return false;
372✔
2417
    }
2418

2419
    public isCalleeMemberOfNamespace(symbolName: string, nodeWhereUsed: AstNode, namespace?: NamespaceStatement) {
2420
        namespace = namespace ?? nodeWhereUsed.findAncestor<NamespaceStatement>(isNamespaceStatement);
56!
2421

2422
        if (!this.isVariableMemberOfNamespace(symbolName, nodeWhereUsed, namespace)) {
56✔
2423
            return false;
44✔
2424
        }
2425
        const exprType = nodeWhereUsed.getType({ flags: SymbolTypeFlag.runtime });
12✔
2426

2427
        if (isCallableType(exprType) || isClassType(exprType)) {
12!
2428
            return true;
12✔
2429
        }
NEW
2430
        return false;
×
2431
    }
2432

2433
    public isVariableMemberOfNamespace(symbolName: string, nodeWhereUsed: AstNode, namespace?: NamespaceStatement) {
2434
        namespace = namespace ?? nodeWhereUsed.findAncestor<NamespaceStatement>(isNamespaceStatement);
1,724✔
2435
        if (!isNamespaceStatement(namespace)) {
1,724✔
2436
            return false;
1,360✔
2437
        }
2438
        const namespaceParts = namespace.getNameParts();
364✔
2439
        let namespaceType: NamespaceType;
2440
        let symbolTable: SymbolTable = namespace.getSymbolTable();
364✔
2441
        for (const part of namespaceParts) {
364✔
2442
            namespaceType = symbolTable.getSymbolType(part.text, { flags: SymbolTypeFlag.runtime }) as NamespaceType;
634✔
2443
            if (namespaceType) {
634✔
2444
                symbolTable = namespaceType.getMemberTable();
633✔
2445
            } else {
2446
                return false;
1✔
2447
            }
2448
        }
2449

2450
        let varData: ExtraSymbolData = {};
363✔
2451
        nodeWhereUsed.getType({ flags: SymbolTypeFlag.runtime, data: varData });
363✔
2452
        const isFromSameNodeInMemberTable = this.symbolComesFromSameNode(symbolName, varData?.definingNode, namespaceType?.getMemberTable());
363!
2453
        return isFromSameNodeInMemberTable;
363✔
2454
    }
2455

2456
    public isVariableShadowingSomething(symbolName: string, nodeWhereUsed: AstNode) {
2457
        let varData: ExtraSymbolData = {};
6,163✔
2458
        let exprType = nodeWhereUsed.getType({ flags: SymbolTypeFlag.runtime, data: varData });
6,163✔
2459
        if (isReferenceType(exprType)) {
6,163✔
2460
            exprType = (exprType as any).getTarget();
5,769✔
2461
        }
2462
        const namespace = nodeWhereUsed?.findAncestor<NamespaceStatement>(isNamespaceStatement);
6,163!
2463

2464
        if (isNamespaceStatement(namespace)) {
6,163✔
2465
            let namespaceHasSymbol = namespace.getSymbolTable().hasSymbol(symbolName, SymbolTypeFlag.runtime);
71✔
2466
            // check if the namespace has a symbol with the same name, but different definiton
2467
            if (namespaceHasSymbol && !this.symbolComesFromSameNode(symbolName, varData.definingNode, namespace.getSymbolTable())) {
71✔
2468
                return true;
14✔
2469
            }
2470
        }
2471
        const bodyTable = nodeWhereUsed.getRoot().getSymbolTable();
6,149✔
2472
        const hasSymbolAtFileLevel = bodyTable.hasSymbol(symbolName, SymbolTypeFlag.runtime);
6,149✔
2473
        if (hasSymbolAtFileLevel && !this.symbolComesFromSameNode(symbolName, varData.definingNode, bodyTable)) {
6,149✔
2474
            return true;
8✔
2475
        }
2476

2477
        return false;
6,141✔
2478
    }
2479

2480
    public chooseTypeFromCodeOrDocComment(codeType: BscType, docType: BscType, options: GetTypeOptions) {
2481
        let returnType: BscType;
2482
        if (options.preferDocType && docType) {
10,270!
NEW
2483
            returnType = docType;
×
NEW
2484
            if (options.data) {
×
NEW
2485
                options.data.isFromDocComment = true;
×
2486
            }
2487
        } else {
2488
            returnType = codeType;
10,270✔
2489
            if (!returnType && docType) {
10,270✔
2490
                returnType = docType;
65✔
2491
                if (options.data) {
65✔
2492
                    options.data.isFromDocComment = true;
19✔
2493
                }
2494
            }
2495
        }
2496
        return returnType;
10,270✔
2497
    }
2498
}
2499

2500
/**
2501
 * 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,
2502
 * we can't use `object.tag` syntax.
2503
 */
2504
export function standardizePath(stringParts, ...expressions: any[]) {
1✔
2505
    let result: string[] = [];
21,666✔
2506
    for (let i = 0; i < stringParts.length; i++) {
21,666✔
2507
        result.push(stringParts[i], expressions[i]);
215,523✔
2508
    }
2509
    return util.driveLetterToLower(
21,666✔
2510
        rokuDeployStandardizePath(
2511
            result.join('')
2512
        )
2513
    );
2514
}
2515

2516
/**
2517
 * An item that can be coerced into a `Range`
2518
 */
2519
export type RangeLike = { location?: Location } | Location | { range?: Range } | Range | undefined;
2520

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