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

rokucommunity / brighterscript / #13311

22 Nov 2024 08:22PM UTC coverage: 86.806%. Remained the same
#13311

push

web-flow
Merge a320d9302 into 2a6afd921

11836 of 14421 branches covered (82.07%)

Branch coverage included in aggregate %.

192 of 206 new or added lines in 26 files covered. (93.2%)

200 existing lines in 18 files now uncovered.

12869 of 14039 relevant lines covered (91.67%)

32035.86 hits per line

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

91.08
/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, isStringType, 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 { VoidType } from '../../types/VoidType';
1✔
31
import { BscTypeKind } from '../../types/BscTypeKind';
1✔
32
import type { BrsDocWithType } from '../../parser/BrightScriptDocParser';
33
import brsDocParser from '../../parser/BrightScriptDocParser';
1✔
34
import type { Location } from 'vscode-languageserver';
35

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

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

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

62
    /**
63
     * The event currently being processed. This will change multiple times throughout the lifetime of this validator
64
     */
65
    private event: OnScopeValidateEvent;
66

67
    private metrics = new Map<string, number>();
1,792✔
68

69

70
    public processEvent(event: OnScopeValidateEvent) {
71
        this.event = event;
3,456✔
72
        if (this.event.program.globalScope === this.event.scope) {
3,456✔
73
            return;
1,791✔
74
        }
75
        this.metrics.clear();
1,665✔
76
        this.walkFiles();
1,665✔
77
        this.currentSegmentBeingValidated = null;
1,661✔
78
        this.flagDuplicateFunctionDeclarations();
1,661✔
79
        this.validateScriptImportPaths();
1,661✔
80
        this.validateClasses();
1,661✔
81
        if (isXmlScope(event.scope)) {
1,661✔
82
            //detect when the child imports a script that its ancestor also imports
83
            this.diagnosticDetectDuplicateAncestorScriptImports(event.scope);
449✔
84
            //validate component interface
85
            this.validateXmlInterface(event.scope);
449✔
86
        }
87

88
        this.event.program.logger.debug(this.event.scope.name, 'metrics:');
1,661✔
89
        let total = 0;
1,661✔
90
        for (const [filePath, num] of this.metrics) {
1,661✔
91
            this.event.program.logger.debug(' - ', filePath, num);
1,629✔
92
            total += num;
1,629✔
93
        }
94
        this.event.program.logger.debug(this.event.scope.name, 'total segments validated', total);
1,661✔
95
    }
96

97
    public reset() {
98
        this.event = undefined;
1,336✔
99
    }
100

101
    private walkFiles() {
102
        const hasChangeInfo = this.event.changedFiles && this.event.changedSymbols;
1,665✔
103

104
        //do many per-file checks for every file in this (and parent) scopes
105
        this.event.scope.enumerateBrsFiles((file) => {
1,665✔
106
            if (!isBrsFile(file)) {
2,045!
UNCOV
107
                return;
×
108
            }
109

110
            const thisFileHasChanges = this.event.changedFiles.includes(file);
2,045✔
111

112
            if (thisFileHasChanges || this.doesFileProvideChangedSymbol(file, this.event.changedSymbols)) {
2,045✔
113
                this.diagnosticDetectFunctionCollisions(file);
1,902✔
114
            }
115
        });
116

117
        this.event.scope.enumerateOwnFiles((file) => {
1,665✔
118
            if (isBrsFile(file)) {
2,484✔
119

120
                const fileUri = util.pathToUri(file.srcPath);
2,035✔
121
                const thisFileHasChanges = this.event.changedFiles.includes(file);
2,035✔
122

123
                const hasUnvalidatedSegments = file.validationSegmenter.hasUnvalidatedSegments();
2,035✔
124

125
                if (hasChangeInfo && !hasUnvalidatedSegments) {
2,035✔
126
                    return;
402✔
127
                }
128

129
                const validationVisitor = createVisitor({
1,633✔
130
                    VariableExpression: (varExpr) => {
131
                        this.validateVariableAndDottedGetExpressions(file, varExpr);
3,566✔
132
                    },
133
                    DottedGetExpression: (dottedGet) => {
134
                        this.validateVariableAndDottedGetExpressions(file, dottedGet);
1,443✔
135
                    },
136
                    CallExpression: (functionCall) => {
137
                        this.validateCallExpression(file, functionCall);
893✔
138
                        this.validateCreateObjectCall(file, functionCall);
893✔
139
                        this.validateComponentMethods(file, functionCall);
893✔
140
                    },
141
                    CallfuncExpression: (functionCall) => {
142
                        this.validateCallFuncExpression(file, functionCall);
30✔
143
                    },
144
                    ReturnStatement: (returnStatement) => {
145
                        this.validateReturnStatement(file, returnStatement);
337✔
146
                    },
147
                    DottedSetStatement: (dottedSetStmt) => {
148
                        this.validateDottedSetStatement(file, dottedSetStmt);
87✔
149
                    },
150
                    BinaryExpression: (binaryExpr) => {
151
                        this.validateBinaryExpression(file, binaryExpr);
240✔
152
                    },
153
                    UnaryExpression: (unaryExpr) => {
154
                        this.validateUnaryExpression(file, unaryExpr);
33✔
155
                    },
156
                    AssignmentStatement: (assignStmt) => {
157
                        this.validateAssignmentStatement(file, assignStmt);
624✔
158
                        // Note: this also includes For statements
159
                        this.detectShadowedLocalVar(file, {
624✔
160
                            expr: assignStmt,
161
                            name: assignStmt.tokens.name.text,
162
                            type: this.getNodeTypeWrapper(file, assignStmt, { flags: SymbolTypeFlag.runtime }),
163
                            nameRange: assignStmt.tokens.name.location?.range
1,872✔
164
                        });
165
                    },
166
                    AugmentedAssignmentStatement: (binaryExpr) => {
167
                        this.validateBinaryExpression(file, binaryExpr);
48✔
168
                    },
169
                    IncrementStatement: (stmt) => {
170
                        this.validateIncrementStatement(file, stmt);
9✔
171
                    },
172
                    NewExpression: (newExpr) => {
173
                        this.validateNewExpression(file, newExpr);
115✔
174
                    },
175
                    ForEachStatement: (forEachStmt) => {
176
                        this.detectShadowedLocalVar(file, {
24✔
177
                            expr: forEachStmt,
178
                            name: forEachStmt.tokens.item.text,
179
                            type: this.getNodeTypeWrapper(file, forEachStmt, { flags: SymbolTypeFlag.runtime }),
180
                            nameRange: forEachStmt.tokens.item.location?.range
72✔
181
                        });
182
                    },
183
                    FunctionParameterExpression: (funcParam) => {
184
                        this.detectShadowedLocalVar(file, {
1,078✔
185
                            expr: funcParam,
186
                            name: funcParam.tokens.name.text,
187
                            type: this.getNodeTypeWrapper(file, funcParam, { flags: SymbolTypeFlag.runtime }),
188
                            nameRange: funcParam.tokens.name.location?.range
3,234✔
189
                        });
190
                    },
191
                    FunctionExpression: (func) => {
192
                        this.validateFunctionExpressionForReturn(func);
1,956✔
193
                    },
194
                    AstNode: (node) => {
195
                        //check for doc comments
196
                        if (!node.leadingTrivia || node.leadingTrivia.filter(triviaToken => triviaToken.kind === TokenKind.Comment).length === 0) {
23,607✔
197
                            return;
18,136✔
198
                        }
199
                        this.validateDocComments(node);
233✔
200
                    }
201
                });
202
                // validate only what's needed in the file
203

204
                const segmentsToWalkForValidation = thisFileHasChanges
1,633✔
205
                    ? file.validationSegmenter.getAllUnvalidatedSegments()
1,633✔
206
                    : file.validationSegmenter.getSegmentsWithChangedSymbols(this.event.changedSymbols);
207

208
                let segmentsValidated = 0;
1,633✔
209
                for (const segment of segmentsToWalkForValidation) {
1,633✔
210
                    if (!file.validationSegmenter.checkIfSegmentNeedsRevalidation(segment, this.event.changedSymbols)) {
3,100!
UNCOV
211
                        continue;
×
212
                    }
213
                    this.currentSegmentBeingValidated = segment;
3,100✔
214
                    this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, segment: segment, tag: ScopeValidatorDiagnosticTag.Segment });
3,100✔
215
                    segmentsValidated++;
3,100✔
216
                    segment.walk(validationVisitor, {
3,100✔
217
                        walkMode: InsideSegmentWalkMode
218
                    });
219
                    file.markSegmentAsValidated(segment);
3,096✔
220
                    this.currentSegmentBeingValidated = null;
3,096✔
221
                }
222
                this.metrics.set(file.pkgPath, segmentsValidated);
1,629✔
223
            }
224
        });
225
    }
226

227
    private doesFileProvideChangedSymbol(file: BrsFile, changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
228
        if (!changedSymbols) {
143!
UNCOV
229
            return true;
×
230
        }
231
        for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
143✔
232
            const providedSymbolKeysFlag = file.providedSymbols.symbolMap.get(flag).keys();
286✔
233
            const changedSymbolSetForFlag = changedSymbols.get(flag);
286✔
234

235
            for (let providedKey of providedSymbolKeysFlag) {
286✔
236
                if (changedSymbolSetForFlag.has(providedKey)) {
270!
UNCOV
237
                    return true;
×
238
                }
239
            }
240
        }
241
        return false;
143✔
242
    }
243

244
    private currentSegmentBeingValidated: AstNode;
245

246

247
    private isTypeKnown(exprType: BscType) {
248
        let isKnownType = exprType?.isResolvable();
3,667✔
249
        return isKnownType;
3,667✔
250
    }
251

252
    /**
253
     * If this is the lhs of an assignment, we don't need to flag it as unresolved
254
     */
255
    private hasValidDeclaration(expression: Expression, exprType: BscType, definingNode?: AstNode) {
256
        if (!isVariableExpression(expression)) {
3,667✔
257
            return false;
1,031✔
258
        }
259
        let assignmentAncestor: AssignmentStatement;
260
        if (isAssignmentStatement(definingNode) && definingNode.tokens.equals.kind === TokenKind.Equal) {
2,636✔
261
            // this symbol was defined in a "normal" assignment (eg. not a compound assignment)
262
            assignmentAncestor = definingNode;
314✔
263
            return assignmentAncestor?.tokens.name?.text.toLowerCase() === expression?.tokens.name?.text.toLowerCase();
314!
264
        } else if (isFunctionParameterExpression(definingNode)) {
2,322✔
265
            // this symbol was defined in a function param
266
            return true;
474✔
267
        } else {
268
            assignmentAncestor = expression?.findAncestor(isAssignmentStatement);
1,848!
269
        }
270
        return assignmentAncestor?.tokens.name === expression?.tokens.name && isUnionType(exprType);
1,848!
271
    }
272

273
    /**
274
     * Validate every function call to `CreateObject`.
275
     * Ideally we would create better type checking/handling for this, but in the mean time, we know exactly
276
     * what these calls are supposed to look like, and this is a very common thing for brs devs to do, so just
277
     * do this manually for now.
278
     */
279
    protected validateCreateObjectCall(file: BrsFile, call: CallExpression) {
280

281
        //skip non CreateObject function calls
282
        const callName = util.getAllDottedGetPartsAsString(call.callee)?.toLowerCase();
893✔
283
        if (callName !== 'createobject' || !isLiteralExpression(call?.args[0])) {
893!
284
            return;
825✔
285
        }
286
        const firstParamToken = (call?.args[0] as LiteralExpression)?.tokens?.value;
68!
287
        const firstParamStringValue = firstParamToken?.text?.replace(/"/g, '');
68!
288
        if (!firstParamStringValue) {
68!
UNCOV
289
            return;
×
290
        }
291
        const firstParamStringValueLower = firstParamStringValue.toLowerCase();
68✔
292

293
        //if this is a `createObject('roSGNode'` call, only support known sg node types
294
        if (firstParamStringValueLower === 'rosgnode' && isLiteralExpression(call?.args[1])) {
68!
295
            const componentName: Token = call?.args[1]?.tokens.value;
29!
296
            this.checkComponentName(componentName);
29✔
297
            if (call?.args.length !== 2) {
29!
298
                // roSgNode should only ever have 2 args in `createObject`
299
                this.addDiagnostic({
1✔
300
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, [2], call?.args.length),
3!
301
                    location: call.location
302
                });
303
            }
304
        } else if (!platformComponentNames.has(firstParamStringValueLower)) {
39✔
305
            this.addDiagnostic({
7✔
306
                ...DiagnosticMessages.unknownBrightScriptComponent(firstParamStringValue),
307
                location: firstParamToken.location
308
            });
309
        } else {
310
            // This is valid brightscript component
311
            // Test for invalid arg counts
312
            const brightScriptComponent: BRSComponentData = components[firstParamStringValueLower];
32✔
313
            // Valid arg counts for createObject are 1+ number of args for constructor
314
            let validArgCounts = brightScriptComponent?.constructors.map(cnstr => cnstr.params.length + 1);
35!
315
            if (validArgCounts.length === 0) {
32✔
316
                // no constructors for this component, so createObject only takes 1 arg
317
                validArgCounts = [1];
2✔
318
            }
319
            if (!validArgCounts.includes(call?.args.length)) {
32!
320
                // Incorrect number of arguments included in `createObject()`
321
                this.addDiagnostic({
4✔
322
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, validArgCounts, call?.args.length),
12!
323
                    location: call.location
324
                });
325
            }
326

327
            // Test for deprecation
328
            if (brightScriptComponent?.isDeprecated) {
32!
UNCOV
329
                this.addDiagnostic({
×
330
                    ...DiagnosticMessages.deprecatedBrightScriptComponent(firstParamStringValue, brightScriptComponent.deprecatedDescription),
331
                    location: call.location
332
                });
333
            }
334
        }
335

336
    }
337

338
    private checkComponentName(componentName: Token) {
339
        //don't validate any components with a colon in their name (probably component libraries, but regular components can have them too).
340
        if (!componentName || componentName?.text?.includes(':')) {
30!
341
            return;
3✔
342
        }
343
        //add diagnostic for unknown components
344
        const unquotedComponentName = componentName?.text?.replace(/"/g, '');
27!
345
        if (unquotedComponentName && !platformNodeNames.has(unquotedComponentName.toLowerCase()) && !this.event.program.getComponent(unquotedComponentName)) {
27✔
346
            this.addDiagnostic({
4✔
347
                ...DiagnosticMessages.unknownRoSGNode(unquotedComponentName),
348
                location: componentName.location
349
            });
350
        }
351
    }
352

353
    /**
354
     * Validate every method call to `component.callfunc()`, `component.createChild()`, etc.
355
     */
356
    protected validateComponentMethods(file: BrsFile, call: CallExpression) {
357
        const lowerMethodNamesChecked = ['callfunc', 'createchild'];
893✔
358
        if (!isDottedGetExpression(call.callee)) {
893✔
359
            return;
530✔
360
        }
361

362
        const callName = call.callee.tokens?.name?.text?.toLowerCase();
363!
363
        if (!callName || !lowerMethodNamesChecked.includes(callName) || !isLiteralExpression(call?.args[0])) {
363!
364
            return;
354✔
365
        }
366

367
        const callerType = call.callee.obj?.getType({ flags: SymbolTypeFlag.runtime });
9!
368
        if (!isComponentType(callerType)) {
9!
NEW
369
            return;
×
370
        }
371
        const componentNameLower = callerType.toString().toLowerCase();
9✔
372

373
        const firstArgToken = call?.args[0]?.tokens.value;
9!
374
        if (callName === 'createchild') {
9✔
375
            this.checkComponentName(firstArgToken);
1✔
376
        } else if (callName === 'callfunc' && !['rosgnode', 'rosgnodenode'].includes(componentNameLower)) {
8✔
377
            const funcType = util.getCallFuncType(call, firstArgToken, { flags: SymbolTypeFlag.runtime, ignoreCall: true });
7✔
378
            if (!funcType?.isResolvable()) {
7✔
379
                const functionName = firstArgToken.text.replace(/"/g, '');
3✔
380
                const functionFullname = `${callerType.toString()}@.${functionName}`;
3✔
381
                this.addMultiScopeDiagnostic({
3✔
382
                    ...DiagnosticMessages.cannotFindFunction(functionName, functionFullname, callerType.toString()),
383
                    location: firstArgToken?.location
9!
384
                });
385
            } else {
386
                this.validateFunctionCall(file, funcType, firstArgToken.location, call.args, 1);
4✔
387
            }
388
        }
389
    }
390

391

392
    private validateCallExpression(file: BrsFile, expression: CallExpression) {
393
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: {} };
893✔
394
        let funcType = this.getNodeTypeWrapper(file, expression?.callee, getTypeOptions);
893!
395
        if (funcType?.isResolvable() && isClassType(funcType)) {
893✔
396
            // We're calling a class - get the constructor
397
            funcType = funcType.getMemberType('new', getTypeOptions);
124✔
398
        }
399
        const callErrorLocation = expression?.callee?.location;
893!
400
        return this.validateFunctionCall(file, funcType, callErrorLocation, expression.args);
893✔
401

402
    }
403

404
    private validateCallFuncExpression(file: BrsFile, expression: CallfuncExpression) {
405
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: {} };
30✔
406
        const funcType = this.getNodeTypeWrapper(file, expression, { ...getTypeOptions, ignoreCall: true });
30✔
407
        const callErrorLocation = expression.location;
26✔
408
        return this.validateFunctionCall(file, funcType, callErrorLocation, expression.args);
26✔
409
    }
410

411
    /**
412
     * Detect calls to functions with the incorrect number of parameters, or wrong types of arguments
413
     */
414
    private validateFunctionCall(file: BrsFile, funcType: BscType, callErrorLocation: Location, args: Expression[], argOffset = 0) {
919✔
415
        if (!funcType?.isResolvable() || !isTypedFunctionType(funcType)) {
923✔
416
            return;
272✔
417
        }
418

419
        //get min/max parameter count for callable
420
        let minParams = 0;
651✔
421
        let maxParams = 0;
651✔
422
        for (let param of funcType.params) {
651✔
423
            maxParams++;
909✔
424
            //optional parameters must come last, so we can assume that minParams won't increase once we hit
425
            //the first isOptional
426
            if (param.isOptional !== true) {
909✔
427
                minParams++;
494✔
428
            }
429
        }
430
        if (funcType.isVariadic) {
651✔
431
            // function accepts variable number of arguments
432
            maxParams = CallExpression.MaximumArguments;
10✔
433
        }
434
        const argsForCall = argOffset < 1 ? args : args.slice(argOffset);
651✔
435

436
        let expCallArgCount = argsForCall.length;
651✔
437
        if (expCallArgCount > maxParams || expCallArgCount < minParams) {
651✔
438
            let minMaxParamsText = minParams === maxParams ? maxParams + argOffset : `${minParams + argOffset}-${maxParams + argOffset}`;
33✔
439
            this.addMultiScopeDiagnostic({
33✔
440
                ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount + argOffset),
441
                location: callErrorLocation
442
            });
443
        }
444
        let paramIndex = 0;
651✔
445
        for (let arg of argsForCall) {
651✔
446
            const data = {} as ExtraSymbolData;
575✔
447
            let argType = this.getNodeTypeWrapper(file, arg, { flags: SymbolTypeFlag.runtime, data: data });
575✔
448

449
            const paramType = funcType.params[paramIndex]?.type;
575✔
450
            if (!paramType) {
575✔
451
                // unable to find a paramType -- maybe there are more args than params
452
                break;
22✔
453
            }
454

455
            if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
553✔
456
                argType = data.definingNode.getConstructorType();
2✔
457
            }
458

459
            const compatibilityData: TypeCompatibilityData = {};
553✔
460
            const isAllowedArgConversion = this.checkAllowedArgConversions(paramType, argType);
553✔
461
            if (!isAllowedArgConversion && !paramType?.isTypeCompatible(argType, compatibilityData)) {
553!
462
                this.addMultiScopeDiagnostic({
34✔
463
                    ...DiagnosticMessages.argumentTypeMismatch(argType.toString(), paramType.toString(), compatibilityData),
464
                    location: arg.location
465
                });
466
            }
467
            paramIndex++;
553✔
468
        }
469
    }
470

471
    private checkAllowedArgConversions(paramType: BscType, argType: BscType): boolean {
472
        if (isNumberType(argType) && isBooleanType(paramType)) {
553✔
473
            return true;
8✔
474
        }
475
        return false;
545✔
476
    }
477

478

479
    /**
480
     * Detect return statements with incompatible types vs. declared return type
481
     */
482
    private validateReturnStatement(file: BrsFile, returnStmt: ReturnStatement) {
483
        const data: ExtraSymbolData = {};
338✔
484
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: data };
338✔
485
        let funcType = returnStmt.findAncestor(isFunctionExpression)?.getType({ flags: SymbolTypeFlag.typetime });
338✔
486
        if (isTypedFunctionType(funcType)) {
338✔
487
            const actualReturnType = returnStmt?.value
337!
488
                ? this.getNodeTypeWrapper(file, returnStmt?.value, getTypeOptions)
1,321!
489
                : VoidType.instance;
490
            const compatibilityData: TypeCompatibilityData = {};
337✔
491

492
            if (funcType.returnType.isResolvable() && actualReturnType && !funcType.returnType.isTypeCompatible(actualReturnType, compatibilityData)) {
337✔
493
                this.addMultiScopeDiagnostic({
14✔
494
                    ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
495
                    location: returnStmt.value?.location ?? returnStmt.location
84✔
496
                });
497
            }
498
        }
499
    }
500

501
    /**
502
     * Detect assigned type different from expected member type
503
     */
504
    private validateDottedSetStatement(file: BrsFile, dottedSetStmt: DottedSetStatement) {
505
        const typeChainExpectedLHS = [] as TypeChainEntry[];
87✔
506
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
87✔
507

508
        const expectedLHSType = this.getNodeTypeWrapper(file, dottedSetStmt, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
87✔
509
        const actualRHSType = this.getNodeTypeWrapper(file, dottedSetStmt?.value, getTypeOpts);
87!
510
        const compatibilityData: TypeCompatibilityData = {};
87✔
511
        const typeChainScan = util.processTypeChain(typeChainExpectedLHS);
87✔
512
        // check if anything in typeChain is an AA - if so, just allow it
513
        if (typeChainExpectedLHS.find(typeChainItem => isAssociativeArrayType(typeChainItem.type))) {
143✔
514
            // something in the chain is an AA
515
            // treat members as dynamic - they could have been set without the type system's knowledge
516
            return;
37✔
517
        }
518
        if (!expectedLHSType || !expectedLHSType?.isResolvable()) {
50!
519
            this.addMultiScopeDiagnostic({
5✔
520
                ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
521
                location: typeChainScan?.location
15!
522
            });
523
            return;
5✔
524
        }
525

526
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
45✔
527

528
        //Most Component fields can be set with strings
529
        //TODO: be more precise about which fields can actually accept strings
530
        //TODO: if RHS is a string literal, we can do more validation to make sure it's the correct type
531
        if (isComponentType(dottedSetStmt.obj?.getType({ flags: SymbolTypeFlag.runtime }))) {
45!
532
            if (isStringType(actualRHSType)) {
15✔
533
                return;
6✔
534
            }
535
        }
536

537
        if (accessibilityIsOk && !expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
39!
538
            this.addMultiScopeDiagnostic({
7✔
539
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
540
                location: dottedSetStmt.location
541
            });
542
        }
543
    }
544

545
    /**
546
     * Detect when declared type does not match rhs type
547
     */
548
    private validateAssignmentStatement(file: BrsFile, assignStmt: AssignmentStatement) {
549
        if (!assignStmt?.typeExpression) {
624!
550
            // nothing to check
551
            return;
617✔
552
        }
553

554
        const typeChainExpectedLHS = [];
7✔
555
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
7✔
556
        const expectedLHSType = this.getNodeTypeWrapper(file, assignStmt.typeExpression, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
7✔
557
        const actualRHSType = this.getNodeTypeWrapper(file, assignStmt.value, getTypeOpts);
7✔
558
        const compatibilityData: TypeCompatibilityData = {};
7✔
559
        if (!expectedLHSType || !expectedLHSType.isResolvable()) {
7✔
560
            // LHS is not resolvable... handled elsewhere
561
        } else if (!expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
6!
562
            this.addMultiScopeDiagnostic({
1✔
563
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
564
                location: assignStmt.location
565
            });
566
        }
567
    }
568

569
    /**
570
     * Detect invalid use of a binary operator
571
     */
572
    private validateBinaryExpression(file: BrsFile, binaryExpr: BinaryExpression | AugmentedAssignmentStatement) {
573
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
288✔
574

575
        if (util.isInTypeExpression(binaryExpr)) {
288✔
576
            return;
13✔
577
        }
578

579
        let leftType = isBinaryExpression(binaryExpr)
275✔
580
            ? this.getNodeTypeWrapper(file, binaryExpr.left, getTypeOpts)
275✔
581
            : this.getNodeTypeWrapper(file, binaryExpr.item, getTypeOpts);
582
        let rightType = isBinaryExpression(binaryExpr)
275✔
583
            ? this.getNodeTypeWrapper(file, binaryExpr.right, getTypeOpts)
275✔
584
            : this.getNodeTypeWrapper(file, binaryExpr.value, getTypeOpts);
585

586
        if (!leftType.isResolvable() || !rightType.isResolvable()) {
275✔
587
            // Can not find the type. error handled elsewhere
588
            return;
12✔
589
        }
590
        let leftTypeToTest = leftType;
263✔
591
        let rightTypeToTest = rightType;
263✔
592

593
        if (isEnumMemberType(leftType) || isEnumType(leftType)) {
263✔
594
            leftTypeToTest = leftType.underlyingType;
11✔
595
        }
596
        if (isEnumMemberType(rightType) || isEnumType(rightType)) {
263✔
597
            rightTypeToTest = rightType.underlyingType;
10✔
598
        }
599

600
        if (isUnionType(leftType) || isUnionType(rightType)) {
263!
601
            // TODO: it is possible to validate based on innerTypes, but more complicated
602
            // Because you need to verify each combination of types
UNCOV
603
            return;
×
604
        }
605
        const leftIsPrimitive = isPrimitiveType(leftTypeToTest);
263✔
606
        const rightIsPrimitive = isPrimitiveType(rightTypeToTest);
263✔
607
        const leftIsAny = isDynamicType(leftTypeToTest) || isObjectType(leftTypeToTest);
263✔
608
        const rightIsAny = isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest);
263✔
609

610

611
        if (leftIsAny && rightIsAny) {
263✔
612
            // both operands are basically "any" type... ignore;
613
            return;
23✔
614
        } else if ((leftIsAny && rightIsPrimitive) || (leftIsPrimitive && rightIsAny)) {
240✔
615
            // one operand is basically "any" type... ignore;
616
            return;
46✔
617
        }
618
        const opResult = util.binaryOperatorResultType(leftTypeToTest, binaryExpr.tokens.operator, rightTypeToTest);
194✔
619

620
        if (isDynamicType(opResult)) {
194✔
621
            // if the result was dynamic, that means there wasn't a valid operation
622
            this.addMultiScopeDiagnostic({
7✔
623
                ...DiagnosticMessages.operatorTypeMismatch(binaryExpr.tokens.operator.text, leftType.toString(), rightType.toString()),
624
                location: binaryExpr.location
625
            });
626
        }
627
    }
628

629
    /**
630
     * Detect invalid use of a Unary operator
631
     */
632
    private validateUnaryExpression(file: BrsFile, unaryExpr: UnaryExpression) {
633
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
33✔
634

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

637
        if (!rightType.isResolvable()) {
33!
638
            // Can not find the type. error handled elsewhere
UNCOV
639
            return;
×
640
        }
641
        let rightTypeToTest = rightType;
33✔
642
        if (isEnumMemberType(rightType)) {
33!
UNCOV
643
            rightTypeToTest = rightType.underlyingType;
×
644
        }
645

646

647
        if (isUnionType(rightTypeToTest)) {
33✔
648
            // TODO: it is possible to validate based on innerTypes, but more complicated
649
            // Because you need to verify each combination of types
650

651
        } else if (isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest)) {
32✔
652
            // operand is basically "any" type... ignore;
653

654
        } else if (isPrimitiveType(rightType)) {
27!
655
            const opResult = util.unaryOperatorResultType(unaryExpr.tokens.operator, rightTypeToTest);
27✔
656
            if (isDynamicType(opResult)) {
27✔
657
                this.addMultiScopeDiagnostic({
1✔
658
                    ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
659
                    location: unaryExpr.location
660
                });
661
            }
662
        } else {
663
            // rhs is not a primitive, so no binary operator is allowed
UNCOV
664
            this.addMultiScopeDiagnostic({
×
665
                ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
666
                location: unaryExpr.location
667
            });
668
        }
669
    }
670

671
    private validateIncrementStatement(file: BrsFile, incStmt: IncrementStatement) {
672
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
9✔
673

674
        let rightType = this.getNodeTypeWrapper(file, incStmt.value, getTypeOpts);
9✔
675

676
        if (!rightType.isResolvable()) {
9!
677
            // Can not find the type. error handled elsewhere
UNCOV
678
            return;
×
679
        }
680

681
        if (isUnionType(rightType)) {
9!
682
            // TODO: it is possible to validate based on innerTypes, but more complicated
683
            // because you need to verify each combination of types
684
        } else if (isDynamicType(rightType) || isObjectType(rightType)) {
9✔
685
            // operand is basically "any" type... ignore
686
        } else if (isNumberType(rightType)) {
7✔
687
            // operand is a number.. this is ok
688
        } else {
689
            // rhs is not a number, so no increment operator is not allowed
690
            this.addMultiScopeDiagnostic({
1✔
691
                ...DiagnosticMessages.operatorTypeMismatch(incStmt.tokens.operator.text, rightType.toString()),
692
                location: incStmt.location
693
            });
694
        }
695
    }
696

697

698
    validateVariableAndDottedGetExpressions(file: BrsFile, expression: VariableExpression | DottedGetExpression) {
699
        if (isDottedGetExpression(expression.parent)) {
5,009✔
700
            // We validate dottedGetExpressions at the top-most level
701
            return;
1,339✔
702
        }
703
        if (isVariableExpression(expression)) {
3,670✔
704
            if (isAssignmentStatement(expression.parent) && expression.parent.tokens.name === expression.tokens.name) {
2,639!
705
                // Don't validate LHS of assignments
706
                return;
×
707
            } else if (isNamespaceStatement(expression.parent)) {
2,639✔
708
                return;
3✔
709
            }
710
        }
711

712
        let symbolType = SymbolTypeFlag.runtime;
3,667✔
713
        let oppositeSymbolType = SymbolTypeFlag.typetime;
3,667✔
714
        const isUsedAsType = util.isInTypeExpression(expression);
3,667✔
715
        if (isUsedAsType) {
3,667✔
716
            // This is used in a TypeExpression - only look up types from SymbolTable
717
            symbolType = SymbolTypeFlag.typetime;
1,181✔
718
            oppositeSymbolType = SymbolTypeFlag.runtime;
1,181✔
719
        }
720

721
        // Do a complete type check on all DottedGet and Variable expressions
722
        // this will create a diagnostic if an invalid member is accessed
723
        const typeChain: TypeChainEntry[] = [];
3,667✔
724
        const typeData = {} as ExtraSymbolData;
3,667✔
725
        let exprType = this.getNodeTypeWrapper(file, expression, {
3,667✔
726
            flags: symbolType,
727
            typeChain: typeChain,
728
            data: typeData
729
        });
730

731
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
3,667!
732

733
        //include a hint diagnostic if this type is marked as deprecated
734
        if (typeData.flags & SymbolTypeFlag.deprecated) { // eslint-disable-line no-bitwise
3,667✔
735
            this.addMultiScopeDiagnostic({
2✔
736
                ...DiagnosticMessages.itemIsDeprecated(),
737
                location: expression.tokens.name.location,
738
                tags: [DiagnosticTag.Deprecated]
739
            });
740
        }
741

742
        if (!this.isTypeKnown(exprType) && !hasValidDeclaration) {
3,667✔
743
            if (this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, isExistenceTest: true })?.isResolvable()) {
231!
744
                const oppoSiteTypeChain = [];
5✔
745
                const invalidlyUsedResolvedType = this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, typeChain: oppoSiteTypeChain, isExistenceTest: true });
5✔
746
                const typeChainScan = util.processTypeChain(oppoSiteTypeChain);
5✔
747
                if (isUsedAsType) {
5✔
748
                    this.addMultiScopeDiagnostic({
2✔
749
                        ...DiagnosticMessages.itemCannotBeUsedAsType(typeChainScan.fullChainName),
750
                        location: expression.location
751
                    });
752
                } else if (invalidlyUsedResolvedType && !isReferenceType(invalidlyUsedResolvedType)) {
3✔
753
                    if (!isAliasStatement(expression.parent)) {
1!
754
                        // alias rhs CAN be a type!
UNCOV
755
                        this.addMultiScopeDiagnostic({
×
756
                            ...DiagnosticMessages.itemCannotBeUsedAsVariable(invalidlyUsedResolvedType.toString()),
757
                            location: expression.location
758
                        });
759
                    }
760
                } else {
761
                    const typeChainScan = util.processTypeChain(typeChain);
2✔
762
                    //if this is a function call, provide a different diagnostic code
763
                    if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
2✔
764
                        this.addMultiScopeDiagnostic({
1✔
765
                            ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
766
                            location: typeChainScan?.location
3!
767
                        });
768
                    } else {
769
                        this.addMultiScopeDiagnostic({
1✔
770
                            ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
771
                            location: typeChainScan?.location
3!
772
                        });
773
                    }
774
                }
775

776
            } else if (!(typeData?.isFromDocComment)) { //|| typeData?.isFromCallFunc)) {
226!
777
                // only show "cannot find... " errors if the type is not defined from a doc comment
778
                const typeChainScan = util.processTypeChain(typeChain);
224✔
779
                if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
224✔
780
                    this.addMultiScopeDiagnostic({
26✔
781
                        ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
782
                        location: typeChainScan?.location
78!
783
                    });
784
                } else {
785
                    this.addMultiScopeDiagnostic({
198✔
786
                        ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
787
                        location: typeChainScan?.location
594!
788
                    });
789
                }
790

791
            }
792
        }
793
        if (isUsedAsType) {
3,667✔
794
            return;
1,181✔
795
        }
796

797
        const containingNamespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,486✔
798
        const containingNamespaceName = containingNamespace?.getName(ParseMode.BrighterScript);
2,486✔
799

800
        if (!(isCallExpression(expression.parent) && isNewExpression(expression.parent?.parent))) {
2,486!
801
            const classUsedAsVarEntry = this.checkTypeChainForClassUsedAsVar(typeChain, containingNamespaceName);
2,371✔
802
            const isClassInNamespace = containingNamespace?.getSymbolTable().hasSymbol(typeChain[0].name, SymbolTypeFlag.runtime);
2,371✔
803
            if (classUsedAsVarEntry && !isClassInNamespace) {
2,371!
804

UNCOV
805
                this.addMultiScopeDiagnostic({
×
806
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable(classUsedAsVarEntry.toString()),
807
                    location: expression.location
808
                });
UNCOV
809
                return;
×
810
            }
811
        }
812

813
        const lastTypeInfo = typeChain[typeChain.length - 1];
2,486✔
814
        const parentTypeInfo = typeChain[typeChain.length - 2];
2,486✔
815

816
        this.checkMemberAccessibility(file, expression, typeChain);
2,486✔
817

818
        if (isNamespaceType(exprType) && !isAliasStatement(expression.parent)) {
2,486✔
819
            this.addMultiScopeDiagnostic({
22✔
820
                ...DiagnosticMessages.itemCannotBeUsedAsVariable('namespace'),
821
                location: expression.location
822
            });
823
        } else if (isEnumType(exprType) && !isAliasStatement(expression.parent)) {
2,464✔
824
            const enumStatement = this.event.scope.getEnum(util.getAllDottedGetPartsAsString(expression));
17✔
825
            if (enumStatement) {
17✔
826
                // there's an enum with this name
827
                this.addMultiScopeDiagnostic({
2✔
828
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable('enum'),
829
                    location: expression.location
830
                });
831
            }
832
        } else if (isDynamicType(exprType) && isEnumType(parentTypeInfo?.type) && isDottedGetExpression(expression)) {
2,447✔
833
            const enumFileLink = this.event.scope.getEnumFileLink(util.getAllDottedGetPartsAsString(expression.obj));
8✔
834
            const typeChainScanForParent = util.processTypeChain(typeChain.slice(0, -1));
8✔
835
            if (enumFileLink) {
8✔
836
                this.addMultiScopeDiagnostic({
5✔
837
                    ...DiagnosticMessages.unknownEnumValue(lastTypeInfo?.name, typeChainScanForParent.fullChainName),
15!
838
                    location: lastTypeInfo?.location,
15!
839
                    relatedInformation: [{
840
                        message: 'Enum declared here',
841
                        location: util.createLocationFromRange(
842
                            util.pathToUri(enumFileLink?.file.srcPath),
15!
843
                            enumFileLink?.item?.tokens.name.location?.range
45!
844
                        )
845
                    }]
846
                });
847
            }
848
        }
849
    }
850

851
    private checkTypeChainForClassUsedAsVar(typeChain: TypeChainEntry[], containingNamespaceName: string) {
852
        const ignoreKinds = [AstNodeKind.TypecastExpression, AstNodeKind.NewExpression];
2,371✔
853
        let lowerNameSoFar = '';
2,371✔
854
        let classUsedAsVar;
855
        let isFirst = true;
2,371✔
856
        for (let i = 0; i < typeChain.length - 1; i++) { // do not look at final entry - we CAN use the constructor as a variable
2,371✔
857
            const tce = typeChain[i];
1,249✔
858
            lowerNameSoFar += `${lowerNameSoFar ? '.' : ''}${tce.name.toLowerCase()}`;
1,249✔
859
            if (!isNamespaceType(tce.type)) {
1,249✔
860
                if (isFirst && containingNamespaceName) {
590✔
861
                    lowerNameSoFar = `${containingNamespaceName.toLowerCase()}.${lowerNameSoFar}`;
71✔
862
                }
863
                if (!tce.astNode || ignoreKinds.includes(tce.astNode.kind)) {
590✔
864
                    break;
12✔
865
                } else if (isClassType(tce.type) && lowerNameSoFar.toLowerCase() === tce.type.name.toLowerCase()) {
578✔
866
                    classUsedAsVar = tce.type;
1✔
867
                }
868
                break;
578✔
869
            }
870
            isFirst = false;
659✔
871
        }
872

873
        return classUsedAsVar;
2,371✔
874
    }
875

876
    /**
877
     * Adds diagnostics for accibility mismatches
878
     *
879
     * @param file file
880
     * @param expression containing expression
881
     * @param typeChain type chain to check
882
     * @returns true if member accesiibility is okay
883
     */
884
    private checkMemberAccessibility(file: BscFile, expression: Expression, typeChain: TypeChainEntry[]) {
885
        for (let i = 0; i < typeChain.length - 1; i++) {
2,531✔
886
            const parentChainItem = typeChain[i];
1,401✔
887
            const childChainItem = typeChain[i + 1];
1,401✔
888
            if (isClassType(parentChainItem.type)) {
1,401✔
889
                const containingClassStmt = expression.findAncestor<ClassStatement>(isClassStatement);
155✔
890
                const classStmtThatDefinesChildMember = childChainItem.data?.definingNode?.findAncestor<ClassStatement>(isClassStatement);
155!
891
                if (classStmtThatDefinesChildMember) {
155✔
892
                    const definingClassName = classStmtThatDefinesChildMember.getName(ParseMode.BrighterScript);
153✔
893
                    const inMatchingClassStmt = containingClassStmt?.getName(ParseMode.BrighterScript).toLowerCase() === parentChainItem.type.name.toLowerCase();
153✔
894
                    // eslint-disable-next-line no-bitwise
895
                    if (childChainItem.data.flags & SymbolTypeFlag.private) {
153✔
896
                        if (!inMatchingClassStmt || childChainItem.data.memberOfAncestor) {
15✔
897
                            this.addMultiScopeDiagnostic({
4✔
898
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
899
                                location: expression.location
900
                            });
901
                            // there's an error... don't worry about the rest of the chain
902
                            return false;
4✔
903
                        }
904
                    }
905

906
                    // eslint-disable-next-line no-bitwise
907
                    if (childChainItem.data.flags & SymbolTypeFlag.protected) {
149✔
908
                        const containingClassName = containingClassStmt?.getName(ParseMode.BrighterScript);
13✔
909
                        const containingNamespaceName = expression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
13✔
910
                        const ancestorClasses = this.event.scope.getClassHierarchy(containingClassName, containingNamespaceName).map(link => link.item);
13✔
911
                        const isSubClassOfDefiningClass = ancestorClasses.includes(classStmtThatDefinesChildMember);
13✔
912

913
                        if (!isSubClassOfDefiningClass) {
13✔
914
                            this.addMultiScopeDiagnostic({
5✔
915
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
916
                                location: expression.location
917
                            });
918
                            // there's an error... don't worry about the rest of the chain
919
                            return false;
5✔
920
                        }
921
                    }
922
                }
923

924
            }
925
        }
926
        return true;
2,522✔
927
    }
928

929
    /**
930
     * Find all "new" statements in the program,
931
     * and make sure we can find a class with that name
932
     */
933
    private validateNewExpression(file: BrsFile, newExpression: NewExpression) {
934
        const newExprType = this.getNodeTypeWrapper(file, newExpression, { flags: SymbolTypeFlag.typetime });
115✔
935
        if (isClassType(newExprType)) {
115✔
936
            return;
107✔
937
        }
938

939
        let potentialClassName = newExpression.className.getName(ParseMode.BrighterScript);
8✔
940
        const namespaceName = newExpression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
8!
941
        let newableClass = this.event.scope.getClass(potentialClassName, namespaceName);
8✔
942

943
        if (!newableClass) {
8!
944
            //try and find functions with this name.
945
            let fullName = util.getFullyQualifiedClassName(potentialClassName, namespaceName);
8✔
946

947
            this.addMultiScopeDiagnostic({
8✔
948
                ...DiagnosticMessages.expressionIsNotConstructable(fullName),
949
                location: newExpression.className.location
950
            });
951

952
        }
953
    }
954

955
    private validateFunctionExpressionForReturn(func: FunctionExpression) {
956
        const returnType = func?.returnTypeExpression?.getType({ flags: SymbolTypeFlag.typetime });
1,956!
957

958
        if (!returnType || !returnType.isResolvable() || isVoidType(returnType) || isDynamicType(returnType)) {
1,956✔
959
            return;
1,744✔
960
        }
961
        const returns = func.body?.findChild<ReturnStatement>(isReturnStatement, { walkMode: WalkMode.visitAll });
212!
962
        if (!returns) {
212✔
963
            this.addMultiScopeDiagnostic({
5✔
964
                ...DiagnosticMessages.expectedReturnStatement(),
965
                location: func.location
966
            });
967
        }
968
    }
969

970
    /**
971
     * Create diagnostics for any duplicate function declarations
972
     */
973
    private flagDuplicateFunctionDeclarations() {
974
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration });
1,661✔
975

976
        //for each list of callables with the same name
977
        for (let [lowerName, callableContainers] of this.event.scope.getCallableContainerMap()) {
1,661✔
978

979
            let globalCallables = [] as CallableContainer[];
124,713✔
980
            let nonGlobalCallables = [] as CallableContainer[];
124,713✔
981
            let ownCallables = [] as CallableContainer[];
124,713✔
982
            let ancestorNonGlobalCallables = [] as CallableContainer[];
124,713✔
983

984

985
            for (let container of callableContainers) {
124,713✔
986
                if (container.scope === this.event.program.globalScope) {
131,391✔
987
                    globalCallables.push(container);
129,558✔
988
                } else {
989
                    nonGlobalCallables.push(container);
1,833✔
990
                    if (container.scope === this.event.scope) {
1,833✔
991
                        ownCallables.push(container);
1,803✔
992
                    } else {
993
                        ancestorNonGlobalCallables.push(container);
30✔
994
                    }
995
                }
996
            }
997

998
            //add info diagnostics about child shadowing parent functions
999
            if (ownCallables.length > 0 && ancestorNonGlobalCallables.length > 0) {
124,713✔
1000
                for (let container of ownCallables) {
24✔
1001
                    //skip the init function (because every component will have one of those){
1002
                    if (lowerName !== 'init') {
24✔
1003
                        let shadowedCallable = ancestorNonGlobalCallables[ancestorNonGlobalCallables.length - 1];
23✔
1004
                        if (!!shadowedCallable && shadowedCallable.callable.file === container.callable.file) {
23✔
1005
                            //same file: skip redundant imports
1006
                            continue;
20✔
1007
                        }
1008
                        this.addMultiScopeDiagnostic({
3✔
1009
                            ...DiagnosticMessages.overridesAncestorFunction(
1010
                                container.callable.name,
1011
                                container.scope.name,
1012
                                shadowedCallable.callable.file.destPath,
1013
                                //grab the last item in the list, which should be the closest ancestor's version
1014
                                shadowedCallable.scope.name
1015
                            ),
1016
                            location: util.createLocationFromFileRange(container.callable.file, container.callable.nameRange)
1017
                        }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
1018
                    }
1019
                }
1020
            }
1021

1022
            //add error diagnostics about duplicate functions in the same scope
1023
            if (ownCallables.length > 1) {
124,713✔
1024

1025
                for (let callableContainer of ownCallables) {
5✔
1026
                    let callable = callableContainer.callable;
10✔
1027
                    const related = [];
10✔
1028
                    for (const ownCallable of ownCallables) {
10✔
1029
                        const thatNameRange = ownCallable.callable.nameRange;
20✔
1030
                        if (ownCallable.callable.nameRange !== callable.nameRange) {
20✔
1031
                            related.push({
10✔
1032
                                message: `Function declared here`,
1033
                                location: util.createLocationFromRange(
1034
                                    util.pathToUri(ownCallable.callable.file?.srcPath),
30!
1035
                                    thatNameRange
1036
                                )
1037
                            });
1038
                        }
1039
                    }
1040

1041
                    this.addMultiScopeDiagnostic({
10✔
1042
                        ...DiagnosticMessages.duplicateFunctionImplementation(callable.name),
1043
                        location: util.createLocationFromFileRange(callable.file, callable.nameRange),
1044
                        relatedInformation: related
1045
                    }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
1046
                }
1047
            }
1048
        }
1049
    }
1050

1051
    /**
1052
     * Verify that all of the scripts imported by each file in this scope actually exist, and have the correct case
1053
     */
1054
    private validateScriptImportPaths() {
1055
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Imports });
1,661✔
1056

1057
        let scriptImports = this.event.scope.getOwnScriptImports();
1,661✔
1058
        //verify every script import
1059
        for (let scriptImport of scriptImports) {
1,661✔
1060
            let referencedFile = this.event.scope.getFileByRelativePath(scriptImport.destPath);
519✔
1061
            //if we can't find the file
1062
            if (!referencedFile) {
519✔
1063
                //skip the default bslib file, it will exist at transpile time but should not show up in the program during validation cycle
1064
                if (scriptImport.destPath === this.event.program.bslibPkgPath) {
16✔
1065
                    continue;
2✔
1066
                }
1067
                let dInfo: DiagnosticInfo;
1068
                if (scriptImport.text.trim().length === 0) {
14✔
1069
                    dInfo = DiagnosticMessages.scriptSrcCannotBeEmpty();
1✔
1070
                } else {
1071
                    dInfo = DiagnosticMessages.referencedFileDoesNotExist();
13✔
1072
                }
1073

1074
                this.addMultiScopeDiagnostic({
14✔
1075
                    ...dInfo,
1076
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1077
                }, ScopeValidatorDiagnosticTag.Imports);
1078
                //if the character casing of the script import path does not match that of the actual path
1079
            } else if (scriptImport.destPath !== referencedFile.destPath) {
503✔
1080
                this.addMultiScopeDiagnostic({
2✔
1081
                    ...DiagnosticMessages.scriptImportCaseMismatch(referencedFile.destPath),
1082
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1083
                }, ScopeValidatorDiagnosticTag.Imports);
1084
            }
1085
        }
1086
    }
1087

1088
    /**
1089
     * Validate all classes defined in this scope
1090
     */
1091
    private validateClasses() {
1092
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Classes });
1,661✔
1093

1094
        let validator = new BsClassValidator(this.event.scope);
1,661✔
1095
        validator.validate();
1,661✔
1096
        for (const diagnostic of validator.diagnostics) {
1,661✔
1097
            this.addMultiScopeDiagnostic({
29✔
1098
                ...diagnostic
1099
            }, ScopeValidatorDiagnosticTag.Classes);
1100
        }
1101
    }
1102

1103

1104
    /**
1105
     * Find various function collisions
1106
     */
1107
    private diagnosticDetectFunctionCollisions(file: BrsFile) {
1108
        const fileUri = util.pathToUri(file.srcPath);
1,902✔
1109
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, tag: ScopeValidatorDiagnosticTag.FunctionCollisions });
1,902✔
1110
        for (let func of file.callables) {
1,902✔
1111
            const funcName = func.getName(ParseMode.BrighterScript);
1,676✔
1112
            const lowerFuncName = funcName?.toLowerCase();
1,676!
1113
            if (lowerFuncName) {
1,676!
1114

1115
                //find function declarations with the same name as a stdlib function
1116
                if (globalCallableMap.has(lowerFuncName)) {
1,676✔
1117
                    this.addMultiScopeDiagnostic({
5✔
1118
                        ...DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction(),
1119
                        location: util.createLocationFromRange(fileUri, func.nameRange)
1120

1121
                    });
1122
                }
1123
            }
1124
        }
1125
    }
1126

1127
    public detectShadowedLocalVar(file: BrsFile, varDeclaration: { expr: AstNode; name: string; type: BscType; nameRange: Range }) {
1128
        const varName = varDeclaration.name;
1,726✔
1129
        const lowerVarName = varName.toLowerCase();
1,726✔
1130
        const callableContainerMap = this.event.scope.getCallableContainerMap();
1,726✔
1131
        const containingNamespace = varDeclaration.expr?.findAncestor<NamespaceStatement>(isNamespaceStatement);
1,726!
1132
        const localVarIsInNamespace = util.isVariableMemberOfNamespace(varDeclaration.name, varDeclaration.expr, containingNamespace);
1,726✔
1133

1134
        const varIsFunction = () => {
1,726✔
1135
            return isCallableType(varDeclaration.type);
11✔
1136
        };
1137

1138
        if (
1,726✔
1139
            //has same name as stdlib
1140
            globalCallableMap.has(lowerVarName)
1141
        ) {
1142
            //local var function with same name as stdlib function
1143
            if (varIsFunction()) {
8✔
1144
                this.addMultiScopeDiagnostic({
1✔
1145
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('stdlib'),
1146
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange)
1147
                });
1148
            }
1149
        } else if (callableContainerMap.has(lowerVarName) && !localVarIsInNamespace) {
1,718✔
1150
            const callable = callableContainerMap.get(lowerVarName);
3✔
1151
            //is same name as a callable
1152
            if (varIsFunction()) {
3✔
1153
                this.addMultiScopeDiagnostic({
1✔
1154
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('scope'),
1155
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1156
                    relatedInformation: [{
1157
                        message: 'Function declared here',
1158
                        location: util.createLocationFromFileRange(
1159
                            callable[0].callable.file,
1160
                            callable[0].callable.nameRange
1161
                        )
1162
                    }]
1163
                });
1164
            } else {
1165
                this.addMultiScopeDiagnostic({
2✔
1166
                    ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1167
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1168
                    relatedInformation: [{
1169
                        message: 'Function declared here',
1170
                        location: util.createLocationFromRange(
1171
                            util.pathToUri(callable[0].callable.file.srcPath),
1172
                            callable[0].callable.nameRange
1173
                        )
1174
                    }]
1175
                });
1176
            }
1177
            //has the same name as an in-scope class
1178
        } else if (!localVarIsInNamespace) {
1,715✔
1179
            const classStmtLink = this.event.scope.getClassFileLink(lowerVarName);
1,714✔
1180
            if (classStmtLink) {
1,714✔
1181
                this.addMultiScopeDiagnostic({
3✔
1182
                    ...DiagnosticMessages.localVarSameNameAsClass(classStmtLink?.item?.getName(ParseMode.BrighterScript)),
18!
1183
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1184
                    relatedInformation: [{
1185
                        message: 'Class declared here',
1186
                        location: util.createLocationFromRange(
1187
                            util.pathToUri(classStmtLink.file.srcPath),
1188
                            classStmtLink?.item.tokens.name.location?.range
18!
1189
                        )
1190
                    }]
1191
                });
1192
            }
1193
        }
1194
    }
1195

1196
    private validateXmlInterface(scope: XmlScope) {
1197
        if (!scope.xmlFile.parser.ast?.componentElement?.interfaceElement) {
449!
1198
            return;
403✔
1199
        }
1200
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLInterface });
46!
1201

1202
        const iface = scope.xmlFile.parser.ast.componentElement.interfaceElement;
46✔
1203
        const callableContainerMap = scope.getCallableContainerMap();
46✔
1204
        //validate functions
1205
        for (const func of iface.functions) {
46✔
1206
            const name = func.name;
47✔
1207
            if (!name) {
47✔
1208
                this.addDiagnostic({
3✔
1209
                    ...DiagnosticMessages.xmlTagMissingAttribute(func.tokens.startTagName.text, 'name'),
1210
                    location: func.tokens.startTagName.location
1211
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1212
            } else if (!callableContainerMap.has(name.toLowerCase())) {
44✔
1213
                this.addDiagnostic({
4✔
1214
                    ...DiagnosticMessages.xmlFunctionNotFound(name),
1215
                    location: func.getAttribute('name')?.tokens.value.location
12!
1216
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1217
            }
1218
        }
1219
        //validate fields
1220
        for (const field of iface.fields) {
46✔
1221
            const { id, type, onChange } = field;
29✔
1222
            if (!id) {
29✔
1223
                this.addDiagnostic({
3✔
1224
                    ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'id'),
1225
                    location: field.tokens.startTagName.location
1226
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1227
            }
1228
            if (!type) {
29✔
1229
                if (!field.alias) {
3✔
1230
                    this.addDiagnostic({
2✔
1231
                        ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'type'),
1232
                        location: field.tokens.startTagName.location
1233
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1234
                }
1235
            } else if (!SGFieldTypes.includes(type.toLowerCase())) {
26✔
1236
                this.addDiagnostic({
1✔
1237
                    ...DiagnosticMessages.xmlInvalidFieldType(type),
1238
                    location: field.getAttribute('type')?.tokens.value.location
3!
1239
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1240
            }
1241
            if (onChange) {
29✔
1242
                if (!callableContainerMap.has(onChange.toLowerCase())) {
1!
1243
                    this.addDiagnostic({
1✔
1244
                        ...DiagnosticMessages.xmlFunctionNotFound(onChange),
1245
                        location: field.getAttribute('onchange')?.tokens.value.location
3!
1246
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1247
                }
1248
            }
1249
        }
1250
    }
1251

1252
    private validateDocComments(node: AstNode) {
1253
        const doc = brsDocParser.parseNode(node);
233✔
1254
        for (const docTag of doc.tags) {
233✔
1255
            const docTypeTag = docTag as BrsDocWithType;
28✔
1256
            if (!docTypeTag.typeExpression || !docTypeTag.location) {
28✔
1257
                continue;
1✔
1258
            }
1259
            const foundType = docTypeTag.typeExpression?.getType({ flags: SymbolTypeFlag.typetime });
27!
1260
            if (!foundType?.isResolvable()) {
27!
1261
                this.addMultiScopeDiagnostic({
8✔
1262
                    ...DiagnosticMessages.cannotFindTypeInCommentDoc(docTypeTag.typeString),
1263
                    location: brsDocParser.getTypeLocationFromToken(docTypeTag.token) ?? docTypeTag.location
24!
1264
                });
1265
            }
1266
        }
1267
    }
1268

1269
    /**
1270
     * Detect when a child has imported a script that an ancestor also imported
1271
     */
1272
    private diagnosticDetectDuplicateAncestorScriptImports(scope: XmlScope) {
1273
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLImports });
449!
1274
        if (scope.xmlFile.parentComponent) {
449✔
1275
            //build a lookup of pkg paths -> FileReference so we can more easily look up collisions
1276
            let parentScriptImports = scope.xmlFile.getAncestorScriptTagImports();
34✔
1277
            let lookup = {} as Record<string, FileReference>;
34✔
1278
            for (let parentScriptImport of parentScriptImports) {
34✔
1279
                //keep the first occurance of a pkgPath. Parent imports are first in the array
1280
                if (!lookup[parentScriptImport.destPath]) {
30!
1281
                    lookup[parentScriptImport.destPath] = parentScriptImport;
30✔
1282
                }
1283
            }
1284

1285
            //add warning for every script tag that this file shares with an ancestor
1286
            for (let scriptImport of scope.xmlFile.scriptTagImports) {
34✔
1287
                let ancestorScriptImport = lookup[scriptImport.destPath];
30✔
1288
                if (ancestorScriptImport) {
30✔
1289
                    let ancestorComponent = ancestorScriptImport.sourceFile as XmlFile;
21✔
1290
                    let ancestorComponentName = ancestorComponent.componentName?.text ?? ancestorComponent.destPath;
21!
1291
                    this.addDiagnostic({
21✔
1292
                        location: util.createLocationFromFileRange(scope.xmlFile, scriptImport.filePathRange),
1293
                        ...DiagnosticMessages.unnecessaryScriptImportInChildFromParent(ancestorComponentName)
1294
                    }, ScopeValidatorDiagnosticTag.XMLImports);
1295
                }
1296
            }
1297
        }
1298
    }
1299

1300
    /**
1301
     * Wraps the AstNode.getType() method, so that we can do extra processing on the result based on the current file
1302
     * In particular, since BrightScript does not support Unions, and there's no way to cast them to something else
1303
     * if the result of .getType() is a union, and we're in a .brs (brightScript) file, treat the result as Dynamic
1304
     *
1305
     * Also, for BrightScript parse-mode, if .getType() returns a node type, do not validate unknown members.
1306
     *
1307
     * In most cases, this returns the result of node.getType()
1308
     *
1309
     * @param file the current file being processed
1310
     * @param node the node to get the type of
1311
     * @param getTypeOpts any options to pass to node.getType()
1312
     * @returns the processed result type
1313
     */
1314
    private getNodeTypeWrapper(file: BrsFile, node: AstNode, getTypeOpts: GetTypeOptions) {
1315
        const type = node?.getType(getTypeOpts);
8,350!
1316

1317
        if (file.parseMode === ParseMode.BrightScript) {
8,346✔
1318
            // this is a brightscript file
1319
            const typeChain = getTypeOpts.typeChain;
928✔
1320
            if (typeChain) {
928✔
1321
                const hasUnion = typeChain.reduce((hasUnion, tce) => {
309✔
1322
                    return hasUnion || isUnionType(tce.type);
360✔
1323
                }, false);
1324
                if (hasUnion) {
309✔
1325
                    // there was a union somewhere in the typechain
1326
                    return DynamicType.instance;
6✔
1327
                }
1328
            }
1329
            if (isUnionType(type)) {
922✔
1330
                //this is a union
1331
                return DynamicType.instance;
4✔
1332
            }
1333

1334
            if (isComponentType(type)) {
918✔
1335
                // modify type to allow any member access for Node types
1336
                type.changeUnknownMemberToDynamic = true;
15✔
1337
            }
1338
        }
1339

1340
        // by default return the result of node.getType()
1341
        return type;
8,336✔
1342
    }
1343

1344
    private getParentTypeDescriptor(typeChainResult: TypeChainProcessResult) {
1345
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType) {
231✔
1346
            return 'namespace';
117✔
1347
        }
1348
        return 'type';
114✔
1349
    }
1350

1351
    private addDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1352
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
51!
1353
        this.event.program.diagnostics.register(diagnostic, {
51✔
1354
            tags: [diagnosticTag],
1355
            segment: this.currentSegmentBeingValidated
1356
        });
1357
    }
1358

1359
    /**
1360
     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
1361
     */
1362
    private addMultiScopeDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1363
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
465✔
1364
        this.event.program.diagnostics.register(diagnostic, {
465✔
1365
            tags: [diagnosticTag],
1366
            segment: this.currentSegmentBeingValidated,
1367
            scope: this.event.scope
1368
        });
1369
    }
1370
}
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