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

rokucommunity / brighterscript / #13308

22 Nov 2024 02:25PM UTC coverage: 86.801%. Remained the same
#13308

push

web-flow
Merge 332332a1f into 2a6afd921

11833 of 14419 branches covered (82.07%)

Branch coverage included in aggregate %.

191 of 205 new or added lines in 26 files covered. (93.17%)

201 existing lines in 18 files now uncovered.

12868 of 14038 relevant lines covered (91.67%)

32022.22 hits per line

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

90.99
/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,791✔
68

69

70
    public processEvent(event: OnScopeValidateEvent) {
71
        this.event = event;
3,455✔
72
        if (this.event.program.globalScope === this.event.scope) {
3,455✔
73
            return;
1,790✔
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);
450✔
84
            //validate component interface
85
            this.validateXmlInterface(event.scope);
450✔
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,335✔
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,485✔
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,563✔
132
                    },
133
                    DottedGetExpression: (dottedGet) => {
134
                        this.validateVariableAndDottedGetExpressions(file, dottedGet);
1,442✔
135
                    },
136
                    CallExpression: (functionCall) => {
137
                        this.validateCallExpression(file, functionCall);
890✔
138
                        this.validateCreateObjectCall(file, functionCall);
890✔
139
                        this.validateComponentMethods(file, functionCall);
890✔
140
                    },
141
                    CallfuncExpression: (functionCall) => {
142
                        this.validateCallFuncExpression(file, functionCall);
30✔
143
                    },
144
                    ReturnStatement: (returnStatement) => {
145
                        this.validateReturnStatement(file, returnStatement);
338✔
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);
622✔
158
                        // Note: this also includes For statements
159
                        this.detectShadowedLocalVar(file, {
622✔
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,866✔
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,587✔
197
                            return;
18,122✔
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,664✔
249
        return isKnownType;
3,664✔
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,664✔
257
            return false;
1,030✔
258
        }
259
        let assignmentAncestor: AssignmentStatement;
260
        if (isAssignmentStatement(definingNode) && definingNode.tokens.equals.kind === TokenKind.Equal) {
2,634✔
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,320✔
265
            // this symbol was defined in a function param
266
            return true;
473✔
267
        } else {
268
            assignmentAncestor = expression?.findAncestor(isAssignmentStatement);
1,847!
269
        }
270
        return assignmentAncestor?.tokens.name === expression?.tokens.name && isUnionType(exprType);
1,847!
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();
890✔
283
        if (callName !== 'createobject' || !isLiteralExpression(call?.args[0])) {
890!
284
            return;
824✔
285
        }
286
        const firstParamToken = (call?.args[0] as LiteralExpression)?.tokens?.value;
66!
287
        const firstParamStringValue = firstParamToken?.text?.replace(/"/g, '');
66!
288
        if (!firstParamStringValue) {
66!
UNCOV
289
            return;
×
290
        }
291
        const firstParamStringValueLower = firstParamStringValue.toLowerCase();
66✔
292

293
        //if this is a `createObject('roSGNode'` call, only support known sg node types
294
        if (firstParamStringValueLower === 'rosgnode' && isLiteralExpression(call?.args[1])) {
66!
295
            const componentName: Token = call?.args[1]?.tokens.value;
28!
296
            this.checkComponentName(componentName);
28✔
297
            if (call?.args.length !== 2) {
28!
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)) {
38✔
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];
31✔
313
            // Valid arg counts for createObject are 1+ number of args for constructor
314
            let validArgCounts = brightScriptComponent?.constructors.map(cnstr => cnstr.params.length + 1);
34!
315
            if (validArgCounts.length === 0) {
31✔
316
                // no constructors for this component, so createObject only takes 1 arg
317
                validArgCounts = [1];
2✔
318
            }
319
            if (!validArgCounts.includes(call?.args.length)) {
31!
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) {
31!
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(':')) {
29!
341
            return;
3✔
342
        }
343
        //add diagnostic for unknown components
344
        const unquotedComponentName = componentName?.text?.replace(/"/g, '');
26!
345
        if (unquotedComponentName && !platformNodeNames.has(unquotedComponentName.toLowerCase()) && !this.event.program.getComponent(unquotedComponentName)) {
26✔
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'];
890✔
358
        if (!isDottedGetExpression(call.callee)) {
890✔
359
            return;
528✔
360
        }
361

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

367
        const callerType = call.callee.obj?.getType({ flags: SymbolTypeFlag.runtime });
8!
368
        if (!isComponentType(callerType)) {
8!
NEW
369
            return;
×
370
        }
371
        const firstArgToken = call?.args[0]?.tokens.value;
8!
372
        if (callName === 'createchild') {
8✔
373
            this.checkComponentName(firstArgToken);
1✔
374
        } else if (callName === 'callfunc') {
7!
375
            const funcType = util.getCallFuncType(call, firstArgToken, { flags: SymbolTypeFlag.runtime, ignoreCall: true });
7✔
376
            if (!funcType?.isResolvable()) {
7✔
377
                const functionName = firstArgToken.text.replace(/"/g, '');
2✔
378
                const functionFullname = `${callerType.toString()}@.${functionName}`;
2✔
379
                this.addMultiScopeDiagnostic({
2✔
380
                    ...DiagnosticMessages.cannotFindFunction(functionName, functionFullname, callerType.toString()),
381
                    location: firstArgToken?.location
6!
382
                });
383
            } else {
384
                this.validateFunctionCall(file, funcType, firstArgToken.location, call.args, 1);
5✔
385
            }
386
        }
387
    }
388

389

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

400
    }
401

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

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

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

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

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

453
            if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
548✔
454
                argType = data.definingNode.getConstructorType();
2✔
455
            }
456

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

469
    private checkAllowedArgConversions(paramType: BscType, argType: BscType): boolean {
470
        if (isNumberType(argType) && isBooleanType(paramType)) {
548✔
471
            return true;
8✔
472
        }
473
        return false;
540✔
474
    }
475

476

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

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

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

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

524
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
45✔
525

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

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

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

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

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

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

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

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

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

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

608

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

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

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

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

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

644

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

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

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

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

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

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

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

695

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

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

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

729
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
3,664!
730

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

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

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

789
            }
790
        }
791
        if (isUsedAsType) {
3,664✔
792
            return;
1,182✔
793
        }
794

795
        const containingNamespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,482✔
796
        const containingNamespaceName = containingNamespace?.getName(ParseMode.BrighterScript);
2,482✔
797

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

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

811
        const lastTypeInfo = typeChain[typeChain.length - 1];
2,482✔
812
        const parentTypeInfo = typeChain[typeChain.length - 2];
2,482✔
813

814
        this.checkMemberAccessibility(file, expression, typeChain);
2,482✔
815

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

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

871
        return classUsedAsVar;
2,367✔
872
    }
873

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

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

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

922
            }
923
        }
924
        return true;
2,518✔
925
    }
926

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

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

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

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

950
        }
951
    }
952

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

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

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

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

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

982

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

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

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

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

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

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

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

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

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

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

1101

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

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

1119
                    });
1120
                }
1121
            }
1122
        }
1123
    }
1124

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

1132
        const varIsFunction = () => {
1,724✔
1133
            return isCallableType(varDeclaration.type);
11✔
1134
        };
1135

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

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

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

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

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

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

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

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

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

1338
        // by default return the result of node.getType()
1339
        return type;
8,324✔
1340
    }
1341

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

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

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

© 2025 Coveralls, Inc