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

rokucommunity / brighterscript / #13945

24 Feb 2025 05:59PM UTC coverage: 86.734% (-0.02%) from 86.755%
#13945

push

web-flow
Merge f5af66a89 into c5c4ff29c

12662 of 15432 branches covered (82.05%)

Branch coverage included in aggregate %.

234 of 252 new or added lines in 8 files covered. (92.86%)

21 existing lines in 3 files now uncovered.

13549 of 14788 relevant lines covered (91.62%)

33559.19 hits per line

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

91.8
/src/bscPlugin/validation/ScopeValidator.ts
1
import { DiagnosticTag, type Range } from 'vscode-languageserver';
1✔
2
import { isAliasStatement, isAssignmentStatement, isAssociativeArrayType, isBinaryExpression, isBooleanType, isBrsFile, isCallExpression, isCallableType, isClassStatement, isClassType, isComponentType, isDottedGetExpression, isDynamicType, isEnumMemberType, isEnumType, isFunctionExpression, isFunctionParameterExpression, isLiteralExpression, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberType, isObjectType, isPrimitiveType, isReferenceType, isReturnStatement, isStringTypeLike, isTypedFunctionType, isUnionType, isVariableExpression, isVoidType, isXmlScope } from '../../astUtils/reflection';
1✔
3
import type { DiagnosticInfo } from '../../DiagnosticMessages';
4
import { DiagnosticMessages } from '../../DiagnosticMessages';
1✔
5
import type { BrsFile } from '../../files/BrsFile';
6
import type { BsDiagnostic, CallableContainer, ExtraSymbolData, FileReference, GetTypeOptions, OnScopeValidateEvent, TypeChainEntry, TypeChainProcessResult, TypeCompatibilityData } from '../../interfaces';
7
import { SymbolTypeFlag } from '../../SymbolTypeFlag';
1✔
8
import type { AssignmentStatement, AugmentedAssignmentStatement, ClassStatement, DottedSetStatement, IncrementStatement, NamespaceStatement, ReturnStatement } from '../../parser/Statement';
9
import { util } from '../../util';
1✔
10
import { nodes, components } from '../../roku-types';
1✔
11
import type { BRSComponentData } from '../../roku-types';
12
import type { Token } from '../../lexer/Token';
13
import { AstNodeKind } from '../../parser/AstNode';
1✔
14
import type { AstNode } from '../../parser/AstNode';
15
import type { Expression } from '../../parser/AstNode';
16
import type { VariableExpression, DottedGetExpression, BinaryExpression, UnaryExpression, NewExpression, LiteralExpression, FunctionExpression, CallfuncExpression } from '../../parser/Expression';
17
import { CallExpression } from '../../parser/Expression';
1✔
18
import { createVisitor, WalkMode } from '../../astUtils/visitors';
1✔
19
import type { BscType } from '../../types/BscType';
20
import type { BscFile } from '../../files/BscFile';
21
import { InsideSegmentWalkMode } from '../../AstValidationSegmenter';
1✔
22
import { TokenKind } from '../../lexer/TokenKind';
1✔
23
import { ParseMode } from '../../parser/Parser';
1✔
24
import { BsClassValidator } from '../../validators/ClassValidator';
1✔
25
import { globalCallableMap } from '../../globalCallables';
1✔
26
import type { XmlScope } from '../../XmlScope';
27
import type { XmlFile } from '../../files/XmlFile';
28
import { SGFieldTypes } from '../../parser/SGTypes';
1✔
29
import { DynamicType } from '../../types/DynamicType';
1✔
30
import { BscTypeKind } from '../../types/BscTypeKind';
1✔
31
import type { BrsDocWithType } from '../../parser/BrightScriptDocParser';
32
import brsDocParser from '../../parser/BrightScriptDocParser';
1✔
33
import type { Location } from 'vscode-languageserver';
34
import { InvalidType } from '../../types/InvalidType';
1✔
35
import { VoidType } from '../../types/VoidType';
1✔
36
import { LogLevel } from '../../Logger';
1✔
37
import { Stopwatch } from '../../Stopwatch';
1✔
38
import chalk from 'chalk';
1✔
39

40
/**
41
 * The lower-case names of all platform-included scenegraph nodes
42
 */
43
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
44
const platformNodeNames = nodes ? new Set((Object.values(nodes) as { name: string }[]).map(x => x?.name.toLowerCase())) : new Set();
96!
45
const platformComponentNames = components ? new Set((Object.values(components) as { name: string }[]).map(x => x?.name.toLowerCase())) : new Set();
65!
46

47
const enum ScopeValidatorDiagnosticTag {
1✔
48
    Imports = 'ScopeValidatorImports',
1✔
49
    NamespaceCollisions = 'ScopeValidatorNamespaceCollisions',
1✔
50
    DuplicateFunctionDeclaration = 'ScopeValidatorDuplicateFunctionDeclaration',
1✔
51
    FunctionCollisions = 'ScopeValidatorFunctionCollisions',
1✔
52
    Classes = 'ScopeValidatorClasses',
1✔
53
    XMLInterface = 'ScopeValidatorXML',
1✔
54
    XMLImports = 'ScopeValidatorXMLImports',
1✔
55
    Default = 'ScopeValidator',
1✔
56
    Segment = 'ScopeValidatorSegment'
1✔
57
}
58

59
/**
60
 * A validator that handles all scope validations for a program validation cycle.
61
 * You should create ONE of these to handle all scope events between beforeProgramValidate and afterProgramValidate,
62
 * and call reset() before using it again in the next cycle
63
 */
64
export class ScopeValidator {
1✔
65

66
    /**
67
     * The event currently being processed. This will change multiple times throughout the lifetime of this validator
68
     */
69
    private event: OnScopeValidateEvent;
70

71
    private segmentsMetrics = new Map<string, { segments: number; time: string }>();
1,884✔
72

73
    public processEvent(event: OnScopeValidateEvent) {
74
        this.event = event;
3,658✔
75
        if (this.event.program.globalScope === this.event.scope) {
3,658✔
76
            return;
1,883✔
77
        }
78
        const logger = this.event.program.logger;
1,775✔
79
        const metrics = {
1,775✔
80
            fileWalkTime: '',
81
            flagDuplicateFunctionTime: '',
82
            classValidationTime: '',
83
            scriptImportValidationTime: '',
84
            xmlValidationTime: ''
85
        };
86
        this.segmentsMetrics.clear();
1,775✔
87
        const validationStopwatch = new Stopwatch();
1,775✔
88

89
        logger.time(LogLevel.debug, ['Validating scope', this.event.scope.name], () => {
1,775✔
90
            metrics.fileWalkTime = validationStopwatch.getDurationTextFor(() => {
1,775✔
91
                this.walkFiles();
1,775✔
92
            }).durationText;
93
            this.currentSegmentBeingValidated = null;
1,772✔
94
            metrics.flagDuplicateFunctionTime = validationStopwatch.getDurationTextFor(() => {
1,772✔
95
                this.flagDuplicateFunctionDeclarations();
1,772✔
96
            }).durationText;
97
            metrics.scriptImportValidationTime = validationStopwatch.getDurationTextFor(() => {
1,772✔
98
                this.validateScriptImportPaths();
1,772✔
99
            }).durationText;
100
            metrics.classValidationTime = validationStopwatch.getDurationTextFor(() => {
1,772✔
101
                this.validateClasses();
1,772✔
102
            }).durationText;
103
            metrics.xmlValidationTime = validationStopwatch.getDurationTextFor(() => {
1,772✔
104
                if (isXmlScope(this.event.scope)) {
1,772✔
105
                    //detect when the child imports a script that its ancestor also imports
106
                    this.diagnosticDetectDuplicateAncestorScriptImports(this.event.scope);
478✔
107
                    //validate component interface
108
                    this.validateXmlInterface(this.event.scope);
478✔
109
                }
110
            }).durationText;
111
        });
112
        logger.debug(this.event.scope.name, 'segment metrics:');
1,772✔
113
        let totalSegments = 0;
1,772✔
114
        for (const [filePath, metric] of this.segmentsMetrics) {
1,772✔
115
            this.event.program.logger.debug(' - ', filePath, metric.segments, metric.time);
1,732✔
116
            totalSegments += metric.segments;
1,732✔
117
        }
118
        logger.debug(this.event.scope.name, 'total segments validated', totalSegments);
1,772✔
119
        this.logValidationMetrics(metrics);
1,772✔
120
    }
121

122
    // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
123
    private logValidationMetrics(metrics: { [key: string]: number | string }) {
124
        let logs = [] as string[];
1,772✔
125
        for (const key in metrics) {
1,772✔
126
            logs.push(`${key}=${chalk.yellow(metrics[key].toString())}`);
8,860✔
127
        }
128
        this.event.program.logger.debug(`Validation Metrics (Scope: ${this.event.scope.name}): ${logs.join(', ')}`);
1,772✔
129
    }
130

131
    public reset() {
132
        this.event = undefined;
1,423✔
133
    }
134

135
    private walkFiles() {
136
        const hasChangeInfo = this.event.changedFiles && this.event.changedSymbols;
1,775✔
137

138
        //do many per-file checks for every file in this (and parent) scopes
139
        this.event.scope.enumerateBrsFiles((file) => {
1,775✔
140
            if (!isBrsFile(file)) {
2,150!
141
                return;
×
142
            }
143

144
            const thisFileHasChanges = this.event.changedFiles.includes(file);
2,150✔
145

146
            if (thisFileHasChanges || this.doesFileProvideChangedSymbol(file, this.event.changedSymbols)) {
2,150✔
147
                this.diagnosticDetectFunctionCollisions(file);
2,005✔
148
            }
149
        });
150
        const fileWalkStopWatch = new Stopwatch();
1,775✔
151

152
        this.event.scope.enumerateOwnFiles((file) => {
1,775✔
153
            if (isBrsFile(file)) {
2,618✔
154

155
                if (this.event.program.diagnostics.shouldFilterFile(file)) {
2,140!
NEW
156
                    return;
×
157
                }
158

159
                fileWalkStopWatch.reset();
2,140✔
160
                fileWalkStopWatch.start();
2,140✔
161

162
                const fileUri = util.pathToUri(file.srcPath);
2,140✔
163
                const thisFileHasChanges = this.event.changedFiles.includes(file);
2,140✔
164

165
                const hasUnvalidatedSegments = file.validationSegmenter.hasUnvalidatedSegments();
2,140✔
166

167
                if (hasChangeInfo && !hasUnvalidatedSegments) {
2,140✔
168
                    return;
405✔
169
                }
170

171
                const validationVisitor = createVisitor({
1,735✔
172
                    VariableExpression: (varExpr) => {
173
                        this.validateVariableAndDottedGetExpressions(file, varExpr);
3,918✔
174
                    },
175
                    DottedGetExpression: (dottedGet) => {
176
                        this.validateVariableAndDottedGetExpressions(file, dottedGet);
1,537✔
177
                    },
178
                    CallExpression: (functionCall) => {
179
                        this.validateCallExpression(file, functionCall);
955✔
180
                        this.validateCreateObjectCall(file, functionCall);
955✔
181
                        this.validateComponentMethods(file, functionCall);
955✔
182
                    },
183
                    CallfuncExpression: (functionCall) => {
184
                        this.validateCallFuncExpression(file, functionCall);
48✔
185
                    },
186
                    ReturnStatement: (returnStatement) => {
187
                        this.validateReturnStatement(file, returnStatement);
363✔
188
                    },
189
                    DottedSetStatement: (dottedSetStmt) => {
190
                        this.validateDottedSetStatement(file, dottedSetStmt);
101✔
191
                    },
192
                    BinaryExpression: (binaryExpr) => {
193
                        this.validateBinaryExpression(file, binaryExpr);
279✔
194
                    },
195
                    UnaryExpression: (unaryExpr) => {
196
                        this.validateUnaryExpression(file, unaryExpr);
33✔
197
                    },
198
                    AssignmentStatement: (assignStmt) => {
199
                        this.validateAssignmentStatement(file, assignStmt);
687✔
200
                        // Note: this also includes For statements
201
                        this.detectShadowedLocalVar(file, {
687✔
202
                            expr: assignStmt,
203
                            name: assignStmt.tokens.name.text,
204
                            type: this.getNodeTypeWrapper(file, assignStmt, { flags: SymbolTypeFlag.runtime }),
205
                            nameRange: assignStmt.tokens.name.location?.range
2,061✔
206
                        });
207
                    },
208
                    AugmentedAssignmentStatement: (binaryExpr) => {
209
                        this.validateBinaryExpression(file, binaryExpr);
62✔
210
                    },
211
                    IncrementStatement: (stmt) => {
212
                        this.validateIncrementStatement(file, stmt);
11✔
213
                    },
214
                    NewExpression: (newExpr) => {
215
                        this.validateNewExpression(file, newExpr);
121✔
216
                    },
217
                    ForEachStatement: (forEachStmt) => {
218
                        this.detectShadowedLocalVar(file, {
26✔
219
                            expr: forEachStmt,
220
                            name: forEachStmt.tokens.item.text,
221
                            type: this.getNodeTypeWrapper(file, forEachStmt, { flags: SymbolTypeFlag.runtime }),
222
                            nameRange: forEachStmt.tokens.item.location?.range
78✔
223
                        });
224
                    },
225
                    FunctionParameterExpression: (funcParam) => {
226
                        this.detectShadowedLocalVar(file, {
1,142✔
227
                            expr: funcParam,
228
                            name: funcParam.tokens.name.text,
229
                            type: this.getNodeTypeWrapper(file, funcParam, { flags: SymbolTypeFlag.runtime }),
230
                            nameRange: funcParam.tokens.name.location?.range
3,426✔
231
                        });
232
                    },
233
                    FunctionExpression: (func) => {
234
                        this.validateFunctionExpressionForReturn(func);
2,084✔
235
                    },
236
                    AstNode: (node) => {
237
                        //check for doc comments
238
                        if (!node.leadingTrivia || node.leadingTrivia.filter(triviaToken => triviaToken.kind === TokenKind.Comment).length === 0) {
25,701✔
239
                            return;
19,711✔
240
                        }
241
                        this.validateDocComments(node);
243✔
242
                    }
243
                });
244
                // validate only what's needed in the file
245

246
                const segmentsToWalkForValidation = thisFileHasChanges
1,735✔
247
                    ? file.validationSegmenter.getAllUnvalidatedSegments()
1,735✔
248
                    : file.validationSegmenter.getSegmentsWithChangedSymbols(this.event.changedSymbols);
249

250
                let segmentsValidated = 0;
1,735✔
251

252
                if (thisFileHasChanges) {
1,735✔
253
                    // clear all ScopeValidatorSegment diagnostics for this file
254
                    this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, tag: ScopeValidatorDiagnosticTag.Segment });
1,605✔
255
                }
256

257

258
                for (const segment of segmentsToWalkForValidation) {
1,735✔
259
                    if (!thisFileHasChanges && !file.validationSegmenter.checkIfSegmentNeedsRevalidation(segment, this.event.changedSymbols)) {
3,310!
260
                        continue;
×
261
                    }
262
                    this.currentSegmentBeingValidated = segment;
3,310✔
263
                    if (!thisFileHasChanges) {
3,310✔
264
                        // just clear the affected diagnostics
265
                        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, segment: segment, tag: ScopeValidatorDiagnosticTag.Segment });
134✔
266
                    }
267
                    segmentsValidated++;
3,310✔
268
                    segment.walk(validationVisitor, {
3,310✔
269
                        walkMode: InsideSegmentWalkMode
270
                    });
271
                    file.markSegmentAsValidated(segment);
3,307✔
272
                    this.currentSegmentBeingValidated = null;
3,307✔
273
                }
274
                fileWalkStopWatch.stop();
1,732✔
275
                const timeString = fileWalkStopWatch.getDurationText();
1,732✔
276
                this.segmentsMetrics.set(file.pkgPath, { segments: segmentsValidated, time: timeString });
1,732✔
277
            }
278
        });
279
    }
280

281
    private doesFileProvideChangedSymbol(file: BrsFile, changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
282
        if (!changedSymbols) {
153!
283
            return true;
×
284
        }
285
        for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
153✔
286
            const providedSymbolKeysFlag = file.providedSymbols.symbolMap.get(flag).keys();
306✔
287
            const changedSymbolSetForFlag = changedSymbols.get(flag);
306✔
288

289
            for (let providedKey of providedSymbolKeysFlag) {
306✔
290
                if (changedSymbolSetForFlag.has(providedKey)) {
284✔
291
                    return true;
8✔
292
                }
293
            }
294
        }
295
        return false;
145✔
296
    }
297

298
    private currentSegmentBeingValidated: AstNode;
299

300

301
    private isTypeKnown(exprType: BscType) {
302
        let isKnownType = exprType?.isResolvable();
4,023✔
303
        return isKnownType;
4,023✔
304
    }
305

306
    /**
307
     * If this is the lhs of an assignment, we don't need to flag it as unresolved
308
     */
309
    private hasValidDeclaration(expression: Expression, exprType: BscType, definingNode?: AstNode) {
310
        if (!isVariableExpression(expression)) {
4,023✔
311
            return false;
1,114✔
312
        }
313
        let assignmentAncestor: AssignmentStatement;
314
        if (isAssignmentStatement(definingNode) && definingNode.tokens.equals.kind === TokenKind.Equal) {
2,909✔
315
            // this symbol was defined in a "normal" assignment (eg. not a compound assignment)
316
            assignmentAncestor = definingNode;
347✔
317
            return assignmentAncestor?.tokens.name?.text.toLowerCase() === expression?.tokens.name?.text.toLowerCase();
347!
318
        } else if (isFunctionParameterExpression(definingNode)) {
2,562✔
319
            // this symbol was defined in a function param
320
            return true;
560✔
321
        } else {
322
            assignmentAncestor = expression?.findAncestor(isAssignmentStatement);
2,002!
323
        }
324
        return assignmentAncestor?.tokens.name === expression?.tokens.name && isUnionType(exprType);
2,002!
325
    }
326

327
    /**
328
     * Validate every function call to `CreateObject`.
329
     * Ideally we would create better type checking/handling for this, but in the mean time, we know exactly
330
     * what these calls are supposed to look like, and this is a very common thing for brs devs to do, so just
331
     * do this manually for now.
332
     */
333
    protected validateCreateObjectCall(file: BrsFile, call: CallExpression) {
334

335
        //skip non CreateObject function calls
336
        const callName = util.getAllDottedGetPartsAsString(call.callee)?.toLowerCase();
955✔
337
        if (callName !== 'createobject' || !isLiteralExpression(call?.args[0])) {
955!
338
            return;
882✔
339
        }
340
        const firstParamToken = (call?.args[0] as LiteralExpression)?.tokens?.value;
73!
341
        const firstParamStringValue = firstParamToken?.text?.replace(/"/g, '');
73!
342
        if (!firstParamStringValue) {
73!
343
            return;
×
344
        }
345
        const firstParamStringValueLower = firstParamStringValue.toLowerCase();
73✔
346

347
        //if this is a `createObject('roSGNode'` call, only support known sg node types
348
        if (firstParamStringValueLower === 'rosgnode' && isLiteralExpression(call?.args[1])) {
73!
349
            const componentName: Token = call?.args[1]?.tokens.value;
31!
350
            this.checkComponentName(componentName);
31✔
351
            if (call?.args.length !== 2) {
31!
352
                // roSgNode should only ever have 2 args in `createObject`
353
                this.addDiagnostic({
1✔
354
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, [2], call?.args.length),
3!
355
                    location: call.location
356
                });
357
            }
358
        } else if (!platformComponentNames.has(firstParamStringValueLower)) {
42✔
359
            this.addDiagnostic({
7✔
360
                ...DiagnosticMessages.unknownBrightScriptComponent(firstParamStringValue),
361
                location: firstParamToken.location
362
            });
363
        } else {
364
            // This is valid brightscript component
365
            // Test for invalid arg counts
366
            const brightScriptComponent: BRSComponentData = components[firstParamStringValueLower];
35✔
367
            // Valid arg counts for createObject are 1+ number of args for constructor
368
            let validArgCounts = brightScriptComponent?.constructors.map(cnstr => cnstr.params.length + 1);
36!
369
            if (validArgCounts.length === 0) {
35✔
370
                // no constructors for this component, so createObject only takes 1 arg
371
                validArgCounts = [1];
4✔
372
            }
373
            if (!validArgCounts.includes(call?.args.length)) {
35!
374
                // Incorrect number of arguments included in `createObject()`
375
                this.addDiagnostic({
4✔
376
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, validArgCounts, call?.args.length),
12!
377
                    location: call.location
378
                });
379
            }
380

381
            // Test for deprecation
382
            if (brightScriptComponent?.isDeprecated) {
35!
383
                this.addDiagnostic({
×
384
                    ...DiagnosticMessages.itemIsDeprecated(firstParamStringValue, brightScriptComponent.deprecatedDescription),
385
                    location: call.location
386
                });
387
            }
388
        }
389

390
    }
391

392
    private checkComponentName(componentName: Token) {
393
        //don't validate any components with a colon in their name (probably component libraries, but regular components can have them too).
394
        if (!componentName || componentName?.text?.includes(':')) {
32!
395
            return;
4✔
396
        }
397
        //add diagnostic for unknown components
398
        const unquotedComponentName = componentName?.text?.replace(/"/g, '');
28!
399
        if (unquotedComponentName && !platformNodeNames.has(unquotedComponentName.toLowerCase()) && !this.event.program.getComponent(unquotedComponentName)) {
28✔
400
            this.addDiagnostic({
4✔
401
                ...DiagnosticMessages.unknownRoSGNode(unquotedComponentName),
402
                location: componentName.location
403
            });
404
        }
405
    }
406

407
    /**
408
     * Validate every method call to `component.callfunc()`, `component.createChild()`, etc.
409
     */
410
    protected validateComponentMethods(file: BrsFile, call: CallExpression) {
411
        const lowerMethodNamesChecked = ['callfunc', 'createchild'];
955✔
412
        if (!isDottedGetExpression(call.callee)) {
955✔
413
            return;
569✔
414
        }
415

416
        const callName = call.callee.tokens?.name?.text?.toLowerCase();
386!
417
        if (!callName || !lowerMethodNamesChecked.includes(callName) || !isLiteralExpression(call?.args[0])) {
386!
418
            return;
373✔
419
        }
420

421
        const callerType = call.callee.obj?.getType({ flags: SymbolTypeFlag.runtime });
13!
422
        if (!isComponentType(callerType)) {
13✔
423
            return;
2✔
424
        }
425
        const firstArgToken = call?.args[0]?.tokens.value;
11!
426
        if (callName === 'createchild') {
11✔
427
            this.checkComponentName(firstArgToken);
1✔
428
        } else if (callName === 'callfunc' && !util.isGenericNodeType(callerType)) {
10✔
429
            const funcType = util.getCallFuncType(call, firstArgToken, { flags: SymbolTypeFlag.runtime, ignoreCall: true });
7✔
430
            if (!funcType?.isResolvable()) {
7✔
431
                const functionName = firstArgToken.text.replace(/"/g, '');
3✔
432
                const functionFullname = `${callerType.toString()}@.${functionName}`;
3✔
433
                this.addMultiScopeDiagnostic({
3✔
434
                    ...DiagnosticMessages.cannotFindCallFuncFunction(functionName, functionFullname, callerType.toString()),
435
                    location: firstArgToken?.location
9!
436
                });
437
            } else {
438
                this.validateFunctionCall(file, funcType, firstArgToken.location, call.args, 1);
4✔
439
            }
440
        }
441
    }
442

443

444
    private validateCallExpression(file: BrsFile, expression: CallExpression) {
445
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: {} };
955✔
446
        let funcType = this.getNodeTypeWrapper(file, expression?.callee, getTypeOptions);
955!
447
        if (funcType?.isResolvable() && isClassType(funcType)) {
955✔
448
            // We're calling a class - get the constructor
449
            funcType = funcType.getMemberType('new', getTypeOptions);
131✔
450
        }
451
        const callErrorLocation = expression?.callee?.location;
955!
452
        return this.validateFunctionCall(file, funcType, callErrorLocation, expression.args);
955✔
453

454
    }
455

456
    private validateCallFuncExpression(file: BrsFile, expression: CallfuncExpression) {
457
        const callerType = expression.callee?.getType({ flags: SymbolTypeFlag.runtime });
48!
458
        if (isDynamicType(callerType)) {
48✔
459
            return;
20✔
460
        }
461
        const methodToken = expression.tokens.methodName;
28✔
462
        const methodName = methodToken?.text ?? '';
28✔
463
        const functionFullname = `${callerType.toString()}@.${methodName}`;
28✔
464
        const callErrorLocation = expression.location;
28✔
465
        if (util.isGenericNodeType(callerType) || isObjectType(callerType) || isDynamicType(callerType)) {
28✔
466
            // ignore "general" node
467
            return;
6✔
468
        }
469

470
        if (!isComponentType(callerType)) {
22✔
471
            this.addMultiScopeDiagnostic({
1✔
472
                ...DiagnosticMessages.cannotFindCallFuncFunction(methodName, functionFullname, callerType.toString()),
473
                location: callErrorLocation
474
            });
475
            return;
1✔
476
        }
477

478
        const funcType = util.getCallFuncType(expression, methodToken, { flags: SymbolTypeFlag.runtime, ignoreCall: true });
21✔
479
        if (!funcType?.isResolvable()) {
18✔
480
            this.addMultiScopeDiagnostic({
1✔
481
                ...DiagnosticMessages.cannotFindCallFuncFunction(methodName, functionFullname, callerType.toString()),
482
                location: callErrorLocation
483
            });
484
        }
485
        return this.validateFunctionCall(file, funcType, callErrorLocation, expression.args);
18✔
486
    }
487

488
    /**
489
     * Detect calls to functions with the incorrect number of parameters, or wrong types of arguments
490
     */
491
    private validateFunctionCall(file: BrsFile, funcType: BscType, callErrorLocation: Location, args: Expression[], argOffset = 0) {
973✔
492
        if (!funcType?.isResolvable() || !isTypedFunctionType(funcType)) {
977✔
493
            return;
266✔
494
        }
495

496
        //get min/max parameter count for callable
497
        let minParams = 0;
711✔
498
        let maxParams = 0;
711✔
499
        for (let param of funcType.params) {
711✔
500
            maxParams++;
987✔
501
            //optional parameters must come last, so we can assume that minParams won't increase once we hit
502
            //the first isOptional
503
            if (param.isOptional !== true) {
987✔
504
                minParams++;
533✔
505
            }
506
        }
507
        if (funcType.isVariadic) {
711✔
508
            // function accepts variable number of arguments
509
            maxParams = CallExpression.MaximumArguments;
12✔
510
        }
511
        const argsForCall = argOffset < 1 ? args : args.slice(argOffset);
711✔
512

513
        let expCallArgCount = argsForCall.length;
711✔
514
        if (expCallArgCount > maxParams || expCallArgCount < minParams) {
711✔
515
            let minMaxParamsText = minParams === maxParams ? maxParams + argOffset : `${minParams + argOffset}-${maxParams + argOffset}`;
33✔
516
            this.addMultiScopeDiagnostic({
33✔
517
                ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount + argOffset),
518
                location: callErrorLocation
519
            });
520
        }
521
        let paramIndex = 0;
711✔
522
        for (let arg of argsForCall) {
711✔
523
            const data = {} as ExtraSymbolData;
621✔
524
            let argType = this.getNodeTypeWrapper(file, arg, { flags: SymbolTypeFlag.runtime, data: data });
621✔
525

526
            const paramType = funcType.params[paramIndex]?.type;
621✔
527
            if (!paramType) {
621✔
528
                // unable to find a paramType -- maybe there are more args than params
529
                break;
22✔
530
            }
531

532
            if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
599✔
533
                argType = data.definingNode.getConstructorType();
2✔
534
            }
535

536
            const compatibilityData: TypeCompatibilityData = {};
599✔
537
            const isAllowedArgConversion = this.checkAllowedArgConversions(paramType, argType);
599✔
538
            if (!isAllowedArgConversion && !paramType?.isTypeCompatible(argType, compatibilityData)) {
599!
539
                this.addMultiScopeDiagnostic({
42✔
540
                    ...DiagnosticMessages.argumentTypeMismatch(argType.toString(), paramType.toString(), compatibilityData),
541
                    location: arg.location
542
                });
543
            }
544
            paramIndex++;
599✔
545
        }
546
    }
547

548
    private checkAllowedArgConversions(paramType: BscType, argType: BscType): boolean {
549
        if (isNumberType(argType) && isBooleanType(paramType)) {
599✔
550
            return true;
8✔
551
        }
552
        return false;
591✔
553
    }
554

555

556
    /**
557
     * Detect return statements with incompatible types vs. declared return type
558
     */
559
    private validateReturnStatement(file: BrsFile, returnStmt: ReturnStatement) {
560
        const data: ExtraSymbolData = {};
364✔
561
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: data };
364✔
562
        let funcType = returnStmt.findAncestor(isFunctionExpression)?.getType({ flags: SymbolTypeFlag.typetime });
364✔
563
        if (isTypedFunctionType(funcType)) {
364✔
564
            let actualReturnType = returnStmt?.value
363!
565
                ? this.getNodeTypeWrapper(file, returnStmt?.value, getTypeOptions)
1,413!
566
                : VoidType.instance;
567
            const compatibilityData: TypeCompatibilityData = {};
363✔
568

569
            // `return` statement by itself in non-built-in function will actually result in `invalid`
570
            const valueReturnType = isVoidType(actualReturnType) ? InvalidType.instance : actualReturnType;
363✔
571

572
            if (funcType.returnType.isResolvable()) {
363✔
573
                if (!returnStmt?.value && isVoidType(funcType.returnType)) {
359!
574
                    // allow empty return when function is return `as void`
575
                    // eslint-disable-next-line no-useless-return
576
                    return;
9✔
577
                } else if (!funcType.returnType.isTypeCompatible(valueReturnType, compatibilityData)) {
350✔
578
                    this.addMultiScopeDiagnostic({
14✔
579
                        ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
580
                        location: returnStmt.value?.location ?? returnStmt.location
84✔
581
                    });
582
                }
583
            }
584
        }
585
    }
586

587
    /**
588
     * Detect assigned type different from expected member type
589
     */
590
    private validateDottedSetStatement(file: BrsFile, dottedSetStmt: DottedSetStatement) {
591
        const typeChainExpectedLHS = [] as TypeChainEntry[];
101✔
592
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
101✔
593

594
        const expectedLHSType = this.getNodeTypeWrapper(file, dottedSetStmt, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
101✔
595
        const actualRHSType = this.getNodeTypeWrapper(file, dottedSetStmt?.value, getTypeOpts);
101!
596
        const compatibilityData: TypeCompatibilityData = {};
101✔
597
        const typeChainScan = util.processTypeChain(typeChainExpectedLHS);
101✔
598
        // check if anything in typeChain is an AA - if so, just allow it
599
        if (typeChainExpectedLHS.find(typeChainItem => isAssociativeArrayType(typeChainItem.type))) {
177✔
600
            // something in the chain is an AA
601
            // treat members as dynamic - they could have been set without the type system's knowledge
602
            return;
39✔
603
        }
604
        if (!expectedLHSType || !expectedLHSType?.isResolvable()) {
62!
605
            this.addMultiScopeDiagnostic({
5✔
606
                ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
607
                location: typeChainScan?.location
15!
608
            });
609
            return;
5✔
610
        }
611

612
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
57✔
613

614
        //Most Component fields can be set with strings
615
        //TODO: be more precise about which fields can actually accept strings
616
        //TODO: if RHS is a string literal, we can do more validation to make sure it's the correct type
617
        if (isComponentType(dottedSetStmt.obj?.getType({ flags: SymbolTypeFlag.runtime }))) {
57!
618
            if (isStringTypeLike(actualRHSType)) {
21✔
619
                return;
6✔
620
            }
621
        }
622

623
        if (accessibilityIsOk && !expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
51!
624
            this.addMultiScopeDiagnostic({
12✔
625
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
626
                location: dottedSetStmt.location
627
            });
628
        }
629
    }
630

631
    /**
632
     * Detect when declared type does not match rhs type
633
     */
634
    private validateAssignmentStatement(file: BrsFile, assignStmt: AssignmentStatement) {
635
        if (!assignStmt?.typeExpression) {
687!
636
            // nothing to check
637
            return;
680✔
638
        }
639

640
        const typeChainExpectedLHS = [];
7✔
641
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
7✔
642
        const expectedLHSType = this.getNodeTypeWrapper(file, assignStmt.typeExpression, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
7✔
643
        const actualRHSType = this.getNodeTypeWrapper(file, assignStmt.value, getTypeOpts);
7✔
644
        const compatibilityData: TypeCompatibilityData = {};
7✔
645
        if (!expectedLHSType || !expectedLHSType.isResolvable()) {
7✔
646
            // LHS is not resolvable... handled elsewhere
647
        } else if (!expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
6!
648
            this.addMultiScopeDiagnostic({
1✔
649
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
650
                location: assignStmt.location
651
            });
652
        }
653
    }
654

655
    /**
656
     * Detect invalid use of a binary operator
657
     */
658
    private validateBinaryExpression(file: BrsFile, binaryExpr: BinaryExpression | AugmentedAssignmentStatement) {
659
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
341✔
660

661
        if (util.isInTypeExpression(binaryExpr)) {
341✔
662
            return;
13✔
663
        }
664

665
        let leftType = isBinaryExpression(binaryExpr)
328✔
666
            ? this.getNodeTypeWrapper(file, binaryExpr.left, getTypeOpts)
328✔
667
            : this.getNodeTypeWrapper(file, binaryExpr.item, getTypeOpts);
668
        let rightType = isBinaryExpression(binaryExpr)
328✔
669
            ? this.getNodeTypeWrapper(file, binaryExpr.right, getTypeOpts)
328✔
670
            : this.getNodeTypeWrapper(file, binaryExpr.value, getTypeOpts);
671

672
        if (!leftType || !rightType || !leftType.isResolvable() || !rightType.isResolvable()) {
328✔
673
            // Can not find the type. error handled elsewhere
674
            return;
13✔
675
        }
676

677
        let leftTypeToTest = leftType;
315✔
678
        let rightTypeToTest = rightType;
315✔
679

680
        if (isEnumMemberType(leftType) || isEnumType(leftType)) {
315✔
681
            leftTypeToTest = leftType.underlyingType;
11✔
682
        }
683
        if (isEnumMemberType(rightType) || isEnumType(rightType)) {
315✔
684
            rightTypeToTest = rightType.underlyingType;
10✔
685
        }
686

687
        if (isUnionType(leftType) || isUnionType(rightType)) {
315✔
688
            // TODO: it is possible to validate based on innerTypes, but more complicated
689
            // Because you need to verify each combination of types
690
            return;
26✔
691
        }
692
        const opResult = util.binaryOperatorResultType(leftTypeToTest, binaryExpr.tokens.operator, rightTypeToTest);
289✔
693

694
        if (!opResult) {
289✔
695
            // if the result was dynamic or void, that means there wasn't a valid operation
696
            this.addMultiScopeDiagnostic({
9✔
697
                ...DiagnosticMessages.operatorTypeMismatch(binaryExpr.tokens.operator.text, leftType.toString(), rightType.toString()),
698
                location: binaryExpr.location
699
            });
700
        }
701
    }
702

703
    /**
704
     * Detect invalid use of a Unary operator
705
     */
706
    private validateUnaryExpression(file: BrsFile, unaryExpr: UnaryExpression) {
707
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
33✔
708

709
        let rightType = this.getNodeTypeWrapper(file, unaryExpr.right, getTypeOpts);
33✔
710

711
        if (!rightType.isResolvable()) {
33!
712
            // Can not find the type. error handled elsewhere
713
            return;
×
714
        }
715
        let rightTypeToTest = rightType;
33✔
716
        if (isEnumMemberType(rightType)) {
33!
717
            rightTypeToTest = rightType.underlyingType;
×
718
        }
719

720
        if (isUnionType(rightTypeToTest)) {
33✔
721
            // TODO: it is possible to validate based on innerTypes, but more complicated
722
            // Because you need to verify each combination of types
723

724
        } else if (isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest)) {
32✔
725
            // operand is basically "any" type... ignore;
726

727
        } else if (isPrimitiveType(rightType)) {
29!
728
            const opResult = util.unaryOperatorResultType(unaryExpr.tokens.operator, rightTypeToTest);
29✔
729
            if (!opResult) {
29✔
730
                this.addMultiScopeDiagnostic({
1✔
731
                    ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
732
                    location: unaryExpr.location
733
                });
734
            }
735
        } else {
736
            // rhs is not a primitive, so no binary operator is allowed
737
            this.addMultiScopeDiagnostic({
×
738
                ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
739
                location: unaryExpr.location
740
            });
741
        }
742
    }
743

744
    private validateIncrementStatement(file: BrsFile, incStmt: IncrementStatement) {
745
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
11✔
746

747
        let rightType = this.getNodeTypeWrapper(file, incStmt.value, getTypeOpts);
11✔
748

749
        if (!rightType.isResolvable()) {
11!
750
            // Can not find the type. error handled elsewhere
751
            return;
×
752
        }
753

754
        if (isUnionType(rightType)) {
11✔
755
            // TODO: it is possible to validate based on innerTypes, but more complicated
756
            // because you need to verify each combination of types
757
        } else if (isDynamicType(rightType) || isObjectType(rightType)) {
9✔
758
            // operand is basically "any" type... ignore
759
        } else if (isNumberType(rightType)) {
8✔
760
            // operand is a number.. this is ok
761
        } else {
762
            // rhs is not a number, so no increment operator is not allowed
763
            this.addMultiScopeDiagnostic({
1✔
764
                ...DiagnosticMessages.operatorTypeMismatch(incStmt.tokens.operator.text, rightType.toString()),
765
                location: incStmt.location
766
            });
767
        }
768
    }
769

770

771
    validateVariableAndDottedGetExpressions(file: BrsFile, expression: VariableExpression | DottedGetExpression) {
772
        if (isDottedGetExpression(expression.parent)) {
5,455✔
773
            // We validate dottedGetExpressions at the top-most level
774
            return;
1,429✔
775
        }
776
        if (isVariableExpression(expression)) {
4,026✔
777
            if (isAssignmentStatement(expression.parent) && expression.parent.tokens.name === expression.tokens.name) {
2,912!
778
                // Don't validate LHS of assignments
779
                return;
×
780
            } else if (isNamespaceStatement(expression.parent)) {
2,912✔
781
                return;
3✔
782
            }
783
        }
784

785
        let symbolType = SymbolTypeFlag.runtime;
4,023✔
786
        let oppositeSymbolType = SymbolTypeFlag.typetime;
4,023✔
787
        const isUsedAsType = util.isInTypeExpression(expression);
4,023✔
788
        if (isUsedAsType) {
4,023✔
789
            // This is used in a TypeExpression - only look up types from SymbolTable
790
            symbolType = SymbolTypeFlag.typetime;
1,277✔
791
            oppositeSymbolType = SymbolTypeFlag.runtime;
1,277✔
792
        }
793

794
        // Do a complete type check on all DottedGet and Variable expressions
795
        // this will create a diagnostic if an invalid member is accessed
796
        const typeChain: TypeChainEntry[] = [];
4,023✔
797
        const typeData = {} as ExtraSymbolData;
4,023✔
798
        let exprType = this.getNodeTypeWrapper(file, expression, {
4,023✔
799
            flags: symbolType,
800
            typeChain: typeChain,
801
            data: typeData
802
        });
803

804
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
4,023!
805

806
        //include a hint diagnostic if this type is marked as deprecated
807
        if (typeData.flags & SymbolTypeFlag.deprecated) { // eslint-disable-line no-bitwise
4,023✔
808
            this.addMultiScopeDiagnostic({
2✔
809
                ...DiagnosticMessages.itemIsDeprecated(),
810
                location: expression.tokens.name.location,
811
                tags: [DiagnosticTag.Deprecated]
812
            });
813
        }
814

815
        if (!this.isTypeKnown(exprType) && !hasValidDeclaration) {
4,023✔
816
            if (this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, isExistenceTest: true })?.isResolvable()) {
236!
817
                const oppoSiteTypeChain = [];
5✔
818
                const invalidlyUsedResolvedType = this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, typeChain: oppoSiteTypeChain, isExistenceTest: true });
5✔
819
                const typeChainScan = util.processTypeChain(oppoSiteTypeChain);
5✔
820
                if (isUsedAsType) {
5✔
821
                    this.addMultiScopeDiagnostic({
2✔
822
                        ...DiagnosticMessages.itemCannotBeUsedAsType(typeChainScan.fullChainName),
823
                        location: expression.location
824
                    });
825
                } else if (invalidlyUsedResolvedType && !isReferenceType(invalidlyUsedResolvedType)) {
3✔
826
                    if (!isAliasStatement(expression.parent)) {
1!
827
                        // alias rhs CAN be a type!
828
                        this.addMultiScopeDiagnostic({
×
829
                            ...DiagnosticMessages.itemCannotBeUsedAsVariable(invalidlyUsedResolvedType.toString()),
830
                            location: expression.location
831
                        });
832
                    }
833
                } else {
834
                    const typeChainScan = util.processTypeChain(typeChain);
2✔
835
                    //if this is a function call, provide a different diagnostic code
836
                    if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
2✔
837
                        this.addMultiScopeDiagnostic({
1✔
838
                            ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
839
                            location: typeChainScan?.location
3!
840
                        });
841
                    } else {
842
                        this.addMultiScopeDiagnostic({
1✔
843
                            ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
844
                            location: typeChainScan?.location
3!
845
                        });
846
                    }
847
                }
848

849
            } else if (!(typeData?.isFromDocComment)) {
231!
850
                // only show "cannot find... " errors if the type is not defined from a doc comment
851
                const typeChainScan = util.processTypeChain(typeChain);
229✔
852
                if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
229✔
853
                    this.addMultiScopeDiagnostic({
27✔
854
                        ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
855
                        location: typeChainScan?.location
81!
856
                    });
857
                } else {
858
                    this.addMultiScopeDiagnostic({
202✔
859
                        ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
860
                        location: typeChainScan?.location
606!
861
                    });
862
                }
863

864
            }
865
        }
866
        if (isUsedAsType) {
4,023✔
867
            return;
1,277✔
868
        }
869

870
        const containingNamespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,746✔
871
        const containingNamespaceName = containingNamespace?.getName(ParseMode.BrighterScript);
2,746✔
872

873
        if (!(isCallExpression(expression.parent) && isNewExpression(expression.parent?.parent))) {
2,746!
874
            const classUsedAsVarEntry = this.checkTypeChainForClassUsedAsVar(typeChain, containingNamespaceName);
2,625✔
875
            const isClassInNamespace = containingNamespace?.getSymbolTable().hasSymbol(typeChain[0].name, SymbolTypeFlag.runtime);
2,625✔
876
            if (classUsedAsVarEntry && !isClassInNamespace) {
2,625!
877

878
                this.addMultiScopeDiagnostic({
×
879
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable(classUsedAsVarEntry.toString()),
880
                    location: expression.location
881
                });
882
                return;
×
883
            }
884
        }
885

886
        const lastTypeInfo = typeChain[typeChain.length - 1];
2,746✔
887
        const parentTypeInfo = typeChain[typeChain.length - 2];
2,746✔
888

889
        this.checkMemberAccessibility(file, expression, typeChain);
2,746✔
890

891
        if (isNamespaceType(exprType) && !isAliasStatement(expression.parent)) {
2,746✔
892
            this.addMultiScopeDiagnostic({
24✔
893
                ...DiagnosticMessages.itemCannotBeUsedAsVariable('namespace'),
894
                location: expression.location
895
            });
896
        } else if (isEnumType(exprType) && !isAliasStatement(expression.parent)) {
2,722✔
897
            const enumStatement = this.event.scope.getEnum(util.getAllDottedGetPartsAsString(expression));
25✔
898
            if (enumStatement) {
25✔
899
                // there's an enum with this name
900
                this.addMultiScopeDiagnostic({
2✔
901
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable('enum'),
902
                    location: expression.location
903
                });
904
            }
905
        } else if (isDynamicType(exprType) && isEnumType(parentTypeInfo?.type) && isDottedGetExpression(expression)) {
2,697✔
906
            const enumFileLink = this.event.scope.getEnumFileLink(util.getAllDottedGetPartsAsString(expression.obj));
9✔
907
            const typeChainScanForItem = util.processTypeChain(typeChain);
9✔
908
            const typeChainScanForParent = util.processTypeChain(typeChain.slice(0, -1));
9✔
909
            if (enumFileLink) {
9✔
910
                this.addMultiScopeDiagnostic({
5✔
911
                    ...DiagnosticMessages.cannotFindName(lastTypeInfo?.name, typeChainScanForItem.fullChainName, typeChainScanForParent.fullNameOfItem, 'enum'),
15!
912
                    location: lastTypeInfo?.location,
15!
913
                    relatedInformation: [{
914
                        message: 'Enum declared here',
915
                        location: util.createLocationFromRange(
916
                            util.pathToUri(enumFileLink?.file.srcPath),
15!
917
                            enumFileLink?.item?.tokens.name.location?.range
45!
918
                        )
919
                    }]
920
                });
921
            }
922
        }
923
    }
924

925
    private checkTypeChainForClassUsedAsVar(typeChain: TypeChainEntry[], containingNamespaceName: string) {
926
        const ignoreKinds = [AstNodeKind.TypecastExpression, AstNodeKind.NewExpression];
2,625✔
927
        let lowerNameSoFar = '';
2,625✔
928
        let classUsedAsVar;
929
        let isFirst = true;
2,625✔
930
        for (let i = 0; i < typeChain.length - 1; i++) { // do not look at final entry - we CAN use the constructor as a variable
2,625✔
931
            const tce = typeChain[i];
1,336✔
932
            lowerNameSoFar += `${lowerNameSoFar ? '.' : ''}${tce.name.toLowerCase()}`;
1,336✔
933
            if (!isNamespaceType(tce.type)) {
1,336✔
934
                if (isFirst && containingNamespaceName) {
666✔
935
                    lowerNameSoFar = `${containingNamespaceName.toLowerCase()}.${lowerNameSoFar}`;
75✔
936
                }
937
                if (!tce.astNode || ignoreKinds.includes(tce.astNode.kind)) {
666✔
938
                    break;
14✔
939
                } else if (isClassType(tce.type) && lowerNameSoFar.toLowerCase() === tce.type.name.toLowerCase()) {
652✔
940
                    classUsedAsVar = tce.type;
1✔
941
                }
942
                break;
652✔
943
            }
944
            isFirst = false;
670✔
945
        }
946

947
        return classUsedAsVar;
2,625✔
948
    }
949

950
    /**
951
     * Adds diagnostics for accibility mismatches
952
     *
953
     * @param file file
954
     * @param expression containing expression
955
     * @param typeChain type chain to check
956
     * @returns true if member accesiibility is okay
957
     */
958
    private checkMemberAccessibility(file: BscFile, expression: Expression, typeChain: TypeChainEntry[]) {
959
        for (let i = 0; i < typeChain.length - 1; i++) {
2,803✔
960
            const parentChainItem = typeChain[i];
1,513✔
961
            const childChainItem = typeChain[i + 1];
1,513✔
962
            if (isClassType(parentChainItem.type)) {
1,513✔
963
                const containingClassStmt = expression.findAncestor<ClassStatement>(isClassStatement);
159✔
964
                const classStmtThatDefinesChildMember = childChainItem.data?.definingNode?.findAncestor<ClassStatement>(isClassStatement);
159!
965
                if (classStmtThatDefinesChildMember) {
159✔
966
                    const definingClassName = classStmtThatDefinesChildMember.getName(ParseMode.BrighterScript);
157✔
967
                    const inMatchingClassStmt = containingClassStmt?.getName(ParseMode.BrighterScript).toLowerCase() === parentChainItem.type.name.toLowerCase();
157✔
968
                    // eslint-disable-next-line no-bitwise
969
                    if (childChainItem.data.flags & SymbolTypeFlag.private) {
157✔
970
                        if (!inMatchingClassStmt || childChainItem.data.memberOfAncestor) {
16✔
971
                            this.addMultiScopeDiagnostic({
4✔
972
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
973
                                location: expression.location
974
                            });
975
                            // there's an error... don't worry about the rest of the chain
976
                            return false;
4✔
977
                        }
978
                    }
979

980
                    // eslint-disable-next-line no-bitwise
981
                    if (childChainItem.data.flags & SymbolTypeFlag.protected) {
153✔
982
                        const containingClassName = containingClassStmt?.getName(ParseMode.BrighterScript);
13✔
983
                        const containingNamespaceName = expression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
13✔
984
                        const ancestorClasses = this.event.scope.getClassHierarchy(containingClassName, containingNamespaceName).map(link => link.item);
13✔
985
                        const isSubClassOfDefiningClass = ancestorClasses.includes(classStmtThatDefinesChildMember);
13✔
986

987
                        if (!isSubClassOfDefiningClass) {
13✔
988
                            this.addMultiScopeDiagnostic({
5✔
989
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
990
                                location: expression.location
991
                            });
992
                            // there's an error... don't worry about the rest of the chain
993
                            return false;
5✔
994
                        }
995
                    }
996
                }
997

998
            }
999
        }
1000
        return true;
2,794✔
1001
    }
1002

1003
    /**
1004
     * Find all "new" statements in the program,
1005
     * and make sure we can find a class with that name
1006
     */
1007
    private validateNewExpression(file: BrsFile, newExpression: NewExpression) {
1008
        const newExprType = this.getNodeTypeWrapper(file, newExpression, { flags: SymbolTypeFlag.typetime });
121✔
1009
        if (isClassType(newExprType)) {
121✔
1010
            return;
113✔
1011
        }
1012

1013
        let potentialClassName = newExpression.className.getName(ParseMode.BrighterScript);
8✔
1014
        const namespaceName = newExpression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
8!
1015
        let newableClass = this.event.scope.getClass(potentialClassName, namespaceName);
8✔
1016

1017
        if (!newableClass) {
8!
1018
            //try and find functions with this name.
1019
            let fullName = util.getFullyQualifiedClassName(potentialClassName, namespaceName);
8✔
1020

1021
            this.addMultiScopeDiagnostic({
8✔
1022
                ...DiagnosticMessages.expressionIsNotConstructable(fullName),
1023
                location: newExpression.className.location
1024
            });
1025

1026
        }
1027
    }
1028

1029
    private validateFunctionExpressionForReturn(func: FunctionExpression) {
1030
        const returnType = func?.returnTypeExpression?.getType({ flags: SymbolTypeFlag.typetime });
2,084!
1031

1032
        if (!returnType || !returnType.isResolvable() || isVoidType(returnType) || isDynamicType(returnType)) {
2,084✔
1033
            return;
1,855✔
1034
        }
1035
        const returns = func.body?.findChild<ReturnStatement>(isReturnStatement, { walkMode: WalkMode.visitAll });
229!
1036
        if (!returns && isStringTypeLike(returnType)) {
229✔
1037
            this.addMultiScopeDiagnostic({
5✔
1038
                ...DiagnosticMessages.returnTypeCoercionMismatch(returnType.toString()),
1039
                location: func.location
1040
            });
1041
        }
1042
    }
1043

1044
    /**
1045
     * Create diagnostics for any duplicate function declarations
1046
     */
1047
    private flagDuplicateFunctionDeclarations() {
1048
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration });
1,772✔
1049

1050
        //for each list of callables with the same name
1051
        for (let [lowerName, callableContainers] of this.event.scope.getCallableContainerMap()) {
1,772✔
1052

1053
            let globalCallables = [] as CallableContainer[];
133,053✔
1054
            let nonGlobalCallables = [] as CallableContainer[];
133,053✔
1055
            let ownCallables = [] as CallableContainer[];
133,053✔
1056
            let ancestorNonGlobalCallables = [] as CallableContainer[];
133,053✔
1057

1058

1059
            for (let container of callableContainers) {
133,053✔
1060
                if (container.scope === this.event.program.globalScope) {
140,175✔
1061
                    globalCallables.push(container);
138,216✔
1062
                } else {
1063
                    nonGlobalCallables.push(container);
1,959✔
1064
                    if (container.scope === this.event.scope) {
1,959✔
1065
                        ownCallables.push(container);
1,929✔
1066
                    } else {
1067
                        ancestorNonGlobalCallables.push(container);
30✔
1068
                    }
1069
                }
1070
            }
1071

1072
            //add info diagnostics about child shadowing parent functions
1073
            if (ownCallables.length > 0 && ancestorNonGlobalCallables.length > 0) {
133,053✔
1074
                for (let container of ownCallables) {
24✔
1075
                    //skip the init function (because every component will have one of those){
1076
                    if (lowerName !== 'init') {
24✔
1077
                        let shadowedCallable = ancestorNonGlobalCallables[ancestorNonGlobalCallables.length - 1];
23✔
1078
                        if (!!shadowedCallable && shadowedCallable.callable.file === container.callable.file) {
23✔
1079
                            //same file: skip redundant imports
1080
                            continue;
20✔
1081
                        }
1082
                        this.addMultiScopeDiagnostic({
3✔
1083
                            ...DiagnosticMessages.overridesAncestorFunction(
1084
                                container.callable.name,
1085
                                container.scope.name,
1086
                                shadowedCallable.callable.file.destPath,
1087
                                //grab the last item in the list, which should be the closest ancestor's version
1088
                                shadowedCallable.scope.name
1089
                            ),
1090
                            location: util.createLocationFromFileRange(container.callable.file, container.callable.nameRange)
1091
                        }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
1092
                    }
1093
                }
1094
            }
1095

1096
            //add error diagnostics about duplicate functions in the same scope
1097
            if (ownCallables.length > 1) {
133,053✔
1098

1099
                for (let callableContainer of ownCallables) {
5✔
1100
                    let callable = callableContainer.callable;
10✔
1101
                    const related = [];
10✔
1102
                    for (const ownCallable of ownCallables) {
10✔
1103
                        const thatNameRange = ownCallable.callable.nameRange;
20✔
1104
                        if (ownCallable.callable.nameRange !== callable.nameRange) {
20✔
1105
                            related.push({
10✔
1106
                                message: `Function declared here`,
1107
                                location: util.createLocationFromRange(
1108
                                    util.pathToUri(ownCallable.callable.file?.srcPath),
30!
1109
                                    thatNameRange
1110
                                )
1111
                            });
1112
                        }
1113
                    }
1114

1115
                    this.addMultiScopeDiagnostic({
10✔
1116
                        ...DiagnosticMessages.duplicateFunctionImplementation(callable.name),
1117
                        location: util.createLocationFromFileRange(callable.file, callable.nameRange),
1118
                        relatedInformation: related
1119
                    }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
1120
                }
1121
            }
1122
        }
1123
    }
1124

1125
    /**
1126
     * Verify that all of the scripts imported by each file in this scope actually exist, and have the correct case
1127
     */
1128
    private validateScriptImportPaths() {
1129
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Imports });
1,772✔
1130

1131
        let scriptImports = this.event.scope.getOwnScriptImports();
1,772✔
1132
        //verify every script import
1133
        for (let scriptImport of scriptImports) {
1,772✔
1134
            let referencedFile = this.event.scope.getFileByRelativePath(scriptImport.destPath);
540✔
1135
            //if we can't find the file
1136
            if (!referencedFile) {
540✔
1137
                //skip the default bslib file, it will exist at transpile time but should not show up in the program during validation cycle
1138
                if (scriptImport.destPath === this.event.program.bslibPkgPath) {
16✔
1139
                    continue;
2✔
1140
                }
1141
                let dInfo: DiagnosticInfo;
1142
                if (scriptImport.text.trim().length === 0) {
14✔
1143
                    dInfo = DiagnosticMessages.scriptSrcCannotBeEmpty();
1✔
1144
                } else {
1145
                    dInfo = DiagnosticMessages.referencedFileDoesNotExist();
13✔
1146
                }
1147

1148
                this.addMultiScopeDiagnostic({
14✔
1149
                    ...dInfo,
1150
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1151
                }, ScopeValidatorDiagnosticTag.Imports);
1152
                //if the character casing of the script import path does not match that of the actual path
1153
            } else if (scriptImport.destPath !== referencedFile.destPath) {
524✔
1154
                this.addMultiScopeDiagnostic({
2✔
1155
                    ...DiagnosticMessages.scriptImportCaseMismatch(referencedFile.destPath),
1156
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1157
                }, ScopeValidatorDiagnosticTag.Imports);
1158
            }
1159
        }
1160
    }
1161

1162
    /**
1163
     * Validate all classes defined in this scope
1164
     */
1165
    private validateClasses() {
1166
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Classes });
1,772✔
1167

1168
        let validator = new BsClassValidator(this.event.scope);
1,772✔
1169
        validator.validate();
1,772✔
1170
        for (const diagnostic of validator.diagnostics) {
1,772✔
1171
            this.addMultiScopeDiagnostic({
29✔
1172
                ...diagnostic
1173
            }, ScopeValidatorDiagnosticTag.Classes);
1174
        }
1175
    }
1176

1177

1178
    /**
1179
     * Find various function collisions
1180
     */
1181
    private diagnosticDetectFunctionCollisions(file: BrsFile) {
1182
        const fileUri = util.pathToUri(file.srcPath);
2,005✔
1183
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, tag: ScopeValidatorDiagnosticTag.FunctionCollisions });
2,005✔
1184
        for (let func of file.callables) {
2,005✔
1185
            const funcName = func.getName(ParseMode.BrighterScript);
1,799✔
1186
            const lowerFuncName = funcName?.toLowerCase();
1,799!
1187
            if (lowerFuncName) {
1,799!
1188

1189
                //find function declarations with the same name as a stdlib function
1190
                if (globalCallableMap.has(lowerFuncName)) {
1,799✔
1191
                    this.addMultiScopeDiagnostic({
5✔
1192
                        ...DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction(),
1193
                        location: util.createLocationFromRange(fileUri, func.nameRange)
1194

1195
                    });
1196
                }
1197
            }
1198
        }
1199
    }
1200

1201
    public detectShadowedLocalVar(file: BrsFile, varDeclaration: { expr: AstNode; name: string; type: BscType; nameRange: Range }) {
1202
        const varName = varDeclaration.name;
1,855✔
1203
        const lowerVarName = varName.toLowerCase();
1,855✔
1204
        const callableContainerMap = this.event.scope.getCallableContainerMap();
1,855✔
1205
        const containingNamespace = varDeclaration.expr?.findAncestor<NamespaceStatement>(isNamespaceStatement);
1,855!
1206
        const localVarIsInNamespace = util.isVariableMemberOfNamespace(varDeclaration.name, varDeclaration.expr, containingNamespace);
1,855✔
1207

1208
        const varIsFunction = () => {
1,855✔
1209
            return isCallableType(varDeclaration.type);
11✔
1210
        };
1211

1212
        if (
1,855✔
1213
            //has same name as stdlib
1214
            globalCallableMap.has(lowerVarName)
1215
        ) {
1216
            //local var function with same name as stdlib function
1217
            if (varIsFunction()) {
8✔
1218
                this.addMultiScopeDiagnostic({
1✔
1219
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('stdlib'),
1220
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange)
1221
                });
1222
            }
1223
        } else if (callableContainerMap.has(lowerVarName) && !localVarIsInNamespace) {
1,847✔
1224
            const callable = callableContainerMap.get(lowerVarName);
3✔
1225
            //is same name as a callable
1226
            if (varIsFunction()) {
3✔
1227
                this.addMultiScopeDiagnostic({
1✔
1228
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('scope'),
1229
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1230
                    relatedInformation: [{
1231
                        message: 'Function declared here',
1232
                        location: util.createLocationFromFileRange(
1233
                            callable[0].callable.file,
1234
                            callable[0].callable.nameRange
1235
                        )
1236
                    }]
1237
                });
1238
            } else {
1239
                this.addMultiScopeDiagnostic({
2✔
1240
                    ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1241
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1242
                    relatedInformation: [{
1243
                        message: 'Function declared here',
1244
                        location: util.createLocationFromRange(
1245
                            util.pathToUri(callable[0].callable.file.srcPath),
1246
                            callable[0].callable.nameRange
1247
                        )
1248
                    }]
1249
                });
1250
            }
1251
            //has the same name as an in-scope class
1252
        } else if (!localVarIsInNamespace) {
1,844✔
1253
            const classStmtLink = this.event.scope.getClassFileLink(lowerVarName);
1,843✔
1254
            if (classStmtLink) {
1,843✔
1255
                this.addMultiScopeDiagnostic({
3✔
1256
                    ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1257
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1258
                    relatedInformation: [{
1259
                        message: 'Class declared here',
1260
                        location: util.createLocationFromRange(
1261
                            util.pathToUri(classStmtLink.file.srcPath),
1262
                            classStmtLink?.item.tokens.name.location?.range
18!
1263
                        )
1264
                    }]
1265
                });
1266
            }
1267
        }
1268
    }
1269

1270
    private validateXmlInterface(scope: XmlScope) {
1271
        if (!scope.xmlFile.parser.ast?.componentElement?.interfaceElement) {
478!
1272
            return;
406✔
1273
        }
1274
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLInterface });
72!
1275

1276
        const iface = scope.xmlFile.parser.ast.componentElement.interfaceElement;
72✔
1277
        const callableContainerMap = scope.getCallableContainerMap();
72✔
1278
        //validate functions
1279
        for (const func of iface.functions) {
72✔
1280
            const name = func.name;
62✔
1281
            if (!name) {
62✔
1282
                this.addDiagnostic({
3✔
1283
                    ...DiagnosticMessages.xmlTagMissingAttribute(func.tokens.startTagName.text, 'name'),
1284
                    location: func.tokens.startTagName.location
1285
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1286
            } else if (!callableContainerMap.has(name.toLowerCase())) {
59✔
1287
                this.addDiagnostic({
4✔
1288
                    ...DiagnosticMessages.xmlFunctionNotFound(name),
1289
                    location: func.getAttribute('name')?.tokens.value.location
12!
1290
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1291
            }
1292
        }
1293
        //validate fields
1294
        for (const field of iface.fields) {
72✔
1295
            const { id, type, onChange } = field;
41✔
1296
            if (!id) {
41✔
1297
                this.addDiagnostic({
3✔
1298
                    ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'id'),
1299
                    location: field.tokens.startTagName.location
1300
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1301
            }
1302
            if (!type) {
41✔
1303
                if (!field.alias) {
3✔
1304
                    this.addDiagnostic({
2✔
1305
                        ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'type'),
1306
                        location: field.tokens.startTagName.location
1307
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1308
                }
1309
            } else if (!SGFieldTypes.includes(type.toLowerCase())) {
38✔
1310
                this.addDiagnostic({
1✔
1311
                    ...DiagnosticMessages.xmlInvalidFieldType(type),
1312
                    location: field.getAttribute('type')?.tokens.value.location
3!
1313
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1314
            }
1315
            if (onChange) {
41✔
1316
                if (!callableContainerMap.has(onChange.toLowerCase())) {
1!
1317
                    this.addDiagnostic({
1✔
1318
                        ...DiagnosticMessages.xmlFunctionNotFound(onChange),
1319
                        location: field.getAttribute('onchange')?.tokens.value.location
3!
1320
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1321
                }
1322
            }
1323
        }
1324
    }
1325

1326
    private validateDocComments(node: AstNode) {
1327
        const doc = brsDocParser.parseNode(node);
243✔
1328
        for (const docTag of doc.tags) {
243✔
1329
            const docTypeTag = docTag as BrsDocWithType;
29✔
1330
            if (!docTypeTag.typeExpression || !docTypeTag.location) {
29✔
1331
                continue;
1✔
1332
            }
1333
            const foundType = docTypeTag.typeExpression?.getType({ flags: SymbolTypeFlag.typetime });
28!
1334
            if (!foundType?.isResolvable()) {
28!
1335
                this.addMultiScopeDiagnostic({
8✔
1336
                    ...DiagnosticMessages.cannotFindName(docTypeTag.typeString),
1337
                    location: brsDocParser.getTypeLocationFromToken(docTypeTag.token) ?? docTypeTag.location
24!
1338
                });
1339
            }
1340
        }
1341
    }
1342

1343
    /**
1344
     * Detect when a child has imported a script that an ancestor also imported
1345
     */
1346
    private diagnosticDetectDuplicateAncestorScriptImports(scope: XmlScope) {
1347
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLImports });
478!
1348
        if (scope.xmlFile.parentComponent) {
478✔
1349
            //build a lookup of pkg paths -> FileReference so we can more easily look up collisions
1350
            let parentScriptImports = scope.xmlFile.getAncestorScriptTagImports();
34✔
1351
            let lookup = {} as Record<string, FileReference>;
34✔
1352
            for (let parentScriptImport of parentScriptImports) {
34✔
1353
                //keep the first occurance of a pkgPath. Parent imports are first in the array
1354
                if (!lookup[parentScriptImport.destPath]) {
30!
1355
                    lookup[parentScriptImport.destPath] = parentScriptImport;
30✔
1356
                }
1357
            }
1358

1359
            //add warning for every script tag that this file shares with an ancestor
1360
            for (let scriptImport of scope.xmlFile.scriptTagImports) {
34✔
1361
                let ancestorScriptImport = lookup[scriptImport.destPath];
30✔
1362
                if (ancestorScriptImport) {
30✔
1363
                    let ancestorComponent = ancestorScriptImport.sourceFile as XmlFile;
21✔
1364
                    let ancestorComponentName = ancestorComponent.componentName?.text ?? ancestorComponent.destPath;
21!
1365
                    this.addDiagnostic({
21✔
1366
                        location: util.createLocationFromFileRange(scope.xmlFile, scriptImport.filePathRange),
1367
                        ...DiagnosticMessages.unnecessaryScriptImportInChildFromParent(ancestorComponentName)
1368
                    }, ScopeValidatorDiagnosticTag.XMLImports);
1369
                }
1370
            }
1371
        }
1372
    }
1373

1374
    /**
1375
     * Wraps the AstNode.getType() method, so that we can do extra processing on the result based on the current file
1376
     * In particular, since BrightScript does not support Unions, and there's no way to cast them to something else
1377
     * if the result of .getType() is a union, and we're in a .brs (brightScript) file, treat the result as Dynamic
1378
     *
1379
     * Also, for BrightScript parse-mode, if .getType() returns a node type, do not validate unknown members.
1380
     *
1381
     * In most cases, this returns the result of node.getType()
1382
     *
1383
     * @param file the current file being processed
1384
     * @param node the node to get the type of
1385
     * @param getTypeOpts any options to pass to node.getType()
1386
     * @returns the processed result type
1387
     */
1388
    private getNodeTypeWrapper(file: BrsFile, node: AstNode, getTypeOpts: GetTypeOptions) {
1389
        const type = node?.getType(getTypeOpts);
9,082!
1390

1391
        if (file.parseMode === ParseMode.BrightScript) {
9,082✔
1392
            // this is a brightscript file
1393
            const typeChain = getTypeOpts.typeChain;
964✔
1394
            if (typeChain) {
964✔
1395
                const hasUnion = typeChain.reduce((hasUnion, tce) => {
322✔
1396
                    return hasUnion || isUnionType(tce.type);
374✔
1397
                }, false);
1398
                if (hasUnion) {
322✔
1399
                    // there was a union somewhere in the typechain
1400
                    return DynamicType.instance;
6✔
1401
                }
1402
            }
1403
            if (isUnionType(type)) {
958✔
1404
                //this is a union
1405
                return DynamicType.instance;
4✔
1406
            }
1407

1408
            if (isComponentType(type)) {
954✔
1409
                // modify type to allow any member access for Node types
1410
                type.changeUnknownMemberToDynamic = true;
18✔
1411
            }
1412
        }
1413

1414
        // by default return the result of node.getType()
1415
        return type;
9,072✔
1416
    }
1417

1418
    private getParentTypeDescriptor(typeChainResult: TypeChainProcessResult) {
1419
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType) {
236✔
1420
            return 'namespace';
117✔
1421
        }
1422
        return 'type';
119✔
1423
    }
1424

1425
    private addDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1426
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
51!
1427
        this.event.program.diagnostics.register(diagnostic, {
51✔
1428
            tags: [diagnosticTag],
1429
            segment: this.currentSegmentBeingValidated
1430
        });
1431
    }
1432

1433
    /**
1434
     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
1435
     */
1436
    private addMultiScopeDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1437
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
489✔
1438
        this.event.program.diagnostics.register(diagnostic, {
489✔
1439
            tags: [diagnosticTag],
1440
            segment: this.currentSegmentBeingValidated,
1441
            scope: this.event.scope
1442
        });
1443
    }
1444
}
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